@reddb-io/red-skills 3.18.11 → 3.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
3
3
  import{spawn as L}from"node:child_process";import{isUtf8 as te}from"node:buffer";async function _(e,n=t=>t){let t=[],r=[];return e.stdout?.on("data",i=>t.push(Buffer.from(i))),e.stderr?.on("data",i=>r.push(Buffer.from(i))),await new Promise(i=>{let c=!1,o=async(a,s)=>{if(c)return;c=!0;let l=Buffer.concat(t),u=l;if(l.length>0&&te(l))try{u=await n(l)}catch{}if(process.stdout.write(u),process.stderr.write(Buffer.concat(r)),s){process.kill(process.pid,s),i(128);return}i(a??0)};e.once("error",a=>{r.push(Buffer.from(`${a instanceof Error?a.message:String(a)}
4
- `)),o(127,null)}),e.once("close",(a,s)=>void o(a,s))})}var re=new Set(["dashboard","stats","gains","show","git","gh","vitest","cargo","cat","exec","wait","doctor","status","sweep","setup","mcp","shell-init","server","warm-resident","gh-api-json","hook"]);function R(e){let n=e[0];if(n==="--full"||n==="--brief"||n==="--terse")return e[1]!=="--"||!e[2]?null:{kind:"argv",argv:[...e.slice(2)],level:n.slice(2)};if(!n||n.startsWith("-"))return n!=="--"||!e[1]?null:{kind:"argv",argv:[...e.slice(1)],level:"automatic"};if(n==="proxy"){if(process.env.RSP_PROXY_FAIL_INTERNAL==="1")return null;let t=e.indexOf("--"),r=t>=0?e.slice(1,t):[];if(r.some(a=>a!=="--full"&&a!=="--brief"&&a!=="--terse"))return null;let i=t>=0?e.slice(t+1):e.slice(1);if(i.length!==1||!i[0]?.trim())return null;let c=i[0];if(ie(c))return null;let o=r.includes("--terse")?"terse":r.includes("--brief")?"brief":r.includes("--full")?"full":"automatic";return{kind:"shell",commandLine:c,level:o}}return re.has(n)?null:{kind:"argv",argv:[...e],level:"automatic"}}async function I(e,n){let t=e.kind==="argv"?L(e.argv[0],e.argv.slice(1),{stdio:["inherit","pipe","pipe"]}):L(e.commandLine,{shell:!0,stdio:["inherit","pipe","pipe"]});return await _(t,n)}function ie(e){return/(?:^|&&|\|\||[;|])\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+|env)\s+)*(?:git\s+(?:status|log|diff|show|blame|branch\s+-av)(?:\s|$)|gh\s+(?:pr|issue|run)\s+(?:list|view)(?:\s|$)|vitest(?:\s|$)|cargo\s+test(?:\s|$)|(?:cat|head|tail)\s+)/.test(e)}function F(e){let n={app:e,version:oe(x("__RED_BUILD_VERSION__",()=>"3.18.11")??"0.0.0-dev"),gitSha:x("__RED_BUILD_GIT_SHA__",()=>"f76a806f839e1f527e1e775fe92e720e252f4351")??"unknown",buildTime:x("__RED_BUILD_TIME__",()=>"2026-08-14T22:42:56-03:00")??"unknown",bundleAsset:x("__RED_BUNDLE_ASSET__",()=>"rsp.bundle.min.mjs")??"unknown"},t=x("__REDDB_SDK_VERSION__",()=>"1.23.1"),r=x("__REDDB_BINARY_TAG__",()=>"v1.23.1");return t&&(n.reddbSdkVersion=t),r&&(n.reddbBinaryTag=r),n}function oe(e){return e.startsWith("v")?e.slice(1):e}function N(e){return`${e.app} ${e.version} ${e.gitSha}`}function x(e,n){try{let t=n();return typeof t=="string"&&t.length>0?t:void 0}catch{return process.env[e.replace(/^__|__$/g,"")]}}var D={"=":"data",":=":{to:"data",typed:!0},":":"meta"},se=/^[a-z][a-z0-9+.-]*:\/\//i;function ce(e,n){return typeof n=="string"?{separator:e,to:n,typed:!1,prefix:!1}:{separator:e,to:n.to,typed:n.typed??!1,prefix:n.prefix??!1}}function ae(e=D){return Object.entries(e).map(([n,t])=>ce(n,t)).sort((n,t)=>t.separator.length-n.separator.length)}function ue(e){return/^-\d+(\.\d+)?$/.test(e)}function O(e){return!(!e||e.startsWith("-")&&!ue(e))}function k(e){if(e==="true")return!0;if(e==="false")return!1;if(e==="null")return null;let n=Number(e);return!Number.isNaN(n)&&Number.isFinite(n)&&e.trim()!==""?n:e}function P(e){if(e==="true")return!0;if(e==="false")return!1;if(e==="null")return null;if(e.startsWith("[")&&e.endsWith("]")||e.startsWith("{")&&e.endsWith("}"))try{return JSON.parse(e)}catch{}let n=Number(e);return!Number.isNaN(n)&&Number.isFinite(n)&&e.trim()!==""?n:e.includes(",")?e.split(",").map(t=>P(t.trim())):e}function w(e,n){switch(n){case"boolean":return le(e);case"number":return fe(e);case"array":return e.split(",").map(t=>k(t.trim()));case"string":default:return e}}function le(e){let n=e.toLowerCase().trim();return n==="true"||n==="yes"||n==="1"||n==="on"}function fe(e){if(e.trim()==="")throw new TypeError(`Cannot convert "${e}" to number`);let n=Number(e);if(Number.isNaN(n))throw new TypeError(`Cannot convert "${e}" to number`);return n}function b(e){return e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'")?e.slice(1,-1):e}function de(e,n={}){return e.map(t=>{let r=n[t.type];return r?r(t.value):t.value}).join("")}function d(e,n){return{type:e,value:n}}function pe(e){let n=[],t=he(e);if(n.push(d("section-header","Usage:"),d("text"," "),d("usage-text",t),d("newline",`
4
+ `)),o(127,null)}),e.once("close",(a,s)=>void o(a,s))})}var re=new Set(["dashboard","stats","gains","show","git","gh","vitest","cargo","cat","exec","wait","doctor","status","sweep","setup","mcp","shell-init","server","warm-resident","gh-api-json","hook"]);function R(e){let n=e[0];if(n==="--full"||n==="--brief"||n==="--terse")return e[1]!=="--"||!e[2]?null:{kind:"argv",argv:[...e.slice(2)],level:n.slice(2)};if(!n||n.startsWith("-"))return n!=="--"||!e[1]?null:{kind:"argv",argv:[...e.slice(1)],level:"automatic"};if(n==="proxy"){if(process.env.RSP_PROXY_FAIL_INTERNAL==="1")return null;let t=e.indexOf("--"),r=t>=0?e.slice(1,t):[];if(r.some(a=>a!=="--full"&&a!=="--brief"&&a!=="--terse"))return null;let i=t>=0?e.slice(t+1):e.slice(1);if(i.length!==1||!i[0]?.trim())return null;let c=i[0];if(ie(c))return null;let o=r.includes("--terse")?"terse":r.includes("--brief")?"brief":r.includes("--full")?"full":"automatic";return{kind:"shell",commandLine:c,level:o}}return re.has(n)?null:{kind:"argv",argv:[...e],level:"automatic"}}async function I(e,n){let t=e.kind==="argv"?L(e.argv[0],e.argv.slice(1),{stdio:["inherit","pipe","pipe"]}):L(e.commandLine,{shell:!0,stdio:["inherit","pipe","pipe"]});return await _(t,n)}function ie(e){return/(?:^|&&|\|\||[;|])\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+|env)\s+)*(?:git\s+(?:status|log|diff|show|blame|branch\s+-av)(?:\s|$)|gh\s+(?:pr|issue|run)\s+(?:list|view)(?:\s|$)|vitest(?:\s|$)|cargo\s+test(?:\s|$)|(?:cat|head|tail)\s+)/.test(e)}function F(e){let n={app:e,version:oe(x("__RED_BUILD_VERSION__",()=>"3.19.2")??"0.0.0-dev"),gitSha:x("__RED_BUILD_GIT_SHA__",()=>"81d715b56dbed051a6edfc3eb7f3f659acebe871")??"unknown",buildTime:x("__RED_BUILD_TIME__",()=>"2026-08-18T01:06:20-03:00")??"unknown",bundleAsset:x("__RED_BUNDLE_ASSET__",()=>"rsp.bundle.min.mjs")??"unknown"},t=x("__REDDB_SDK_VERSION__",()=>"1.23.1"),r=x("__REDDB_BINARY_TAG__",()=>"v1.23.1");return t&&(n.reddbSdkVersion=t),r&&(n.reddbBinaryTag=r),n}function oe(e){return e.startsWith("v")?e.slice(1):e}function N(e){return`${e.app} ${e.version} ${e.gitSha}`}function x(e,n){try{let t=n();return typeof t=="string"&&t.length>0?t:void 0}catch{return process.env[e.replace(/^__|__$/g,"")]}}var D={"=":"data",":=":{to:"data",typed:!0},":":"meta"},se=/^[a-z][a-z0-9+.-]*:\/\//i;function ce(e,n){return typeof n=="string"?{separator:e,to:n,typed:!1,prefix:!1}:{separator:e,to:n.to,typed:n.typed??!1,prefix:n.prefix??!1}}function ae(e=D){return Object.entries(e).map(([n,t])=>ce(n,t)).sort((n,t)=>t.separator.length-n.separator.length)}function ue(e){return/^-\d+(\.\d+)?$/.test(e)}function O(e){return!(!e||e.startsWith("-")&&!ue(e))}function k(e){if(e==="true")return!0;if(e==="false")return!1;if(e==="null")return null;let n=Number(e);return!Number.isNaN(n)&&Number.isFinite(n)&&e.trim()!==""?n:e}function P(e){if(e==="true")return!0;if(e==="false")return!1;if(e==="null")return null;if(e.startsWith("[")&&e.endsWith("]")||e.startsWith("{")&&e.endsWith("}"))try{return JSON.parse(e)}catch{}let n=Number(e);return!Number.isNaN(n)&&Number.isFinite(n)&&e.trim()!==""?n:e.includes(",")?e.split(",").map(t=>P(t.trim())):e}function w(e,n){switch(n){case"boolean":return le(e);case"number":return fe(e);case"array":return e.split(",").map(t=>k(t.trim()));case"string":default:return e}}function le(e){let n=e.toLowerCase().trim();return n==="true"||n==="yes"||n==="1"||n==="on"}function fe(e){if(e.trim()==="")throw new TypeError(`Cannot convert "${e}" to number`);let n=Number(e);if(Number.isNaN(n))throw new TypeError(`Cannot convert "${e}" to number`);return n}function b(e){return e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'")?e.slice(1,-1):e}function de(e,n={}){return e.map(t=>{let r=n[t.type];return r?r(t.value):t.value}).join("")}function d(e,n){return{type:e,value:n}}function pe(e){let n=[],t=he(e);if(n.push(d("section-header","Usage:"),d("text"," "),d("usage-text",t),d("newline",`
5
5
  `),d("newline",`
6
6
  `)),e.positional&&e.positional.length>0){n.push(d("section-header","Arguments:"),d("newline",`
7
7
  `));for(let r of e.positional)if(r.description){let i=r.variadic?`${r.name}...`:r.name;n.push(d("indent"," "),d("positional-name",`<${i}>`)),r.required||n.push(d("positional-optional"," (optional)")),n.push(d("newline",`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reddb-io/red-skills",
3
- "version": "3.18.11",
4
- "description": "RedSkills plugin runtime bundles (dev, code-nav, memory, brain) distributed over npm the v2 client transport (ADR 0091). Carries the platform-independent JS bundles inside the tarball plus thin bin shims; no postinstall download.",
3
+ "version": "3.19.2",
4
+ "description": "RedSkills core npm package: compatibility bin shims, marketplace manifests, host generators, and non-plugin supporting runtimes. Plugin bundles ship in @reddb-io/red-skills-<plugin> packages (ADR 0146).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -23,8 +23,20 @@
23
23
  "rsp": "bin/rsp.mjs"
24
24
  },
25
25
  "files": [
26
+ ".agents/plugins/marketplace.json",
27
+ ".claude-plugin/marketplace.json",
28
+ ".gemini-plugin/marketplace.json",
26
29
  "bin/",
27
30
  "dist/",
28
- "README.md"
31
+ "README.md",
32
+ "scripts/generate-codex-manifests.mjs",
33
+ "scripts/generate-gemini-manifests.mjs",
34
+ "scripts/generate-pi-manifests.mjs",
35
+ "scripts/build-gemini-extension.mjs",
36
+ "scripts/validate-gemini-extension.mjs",
37
+ "scripts/install-hermes-skills.mjs",
38
+ "scripts/install-opencode.sh",
39
+ "scripts/install-pi.sh",
40
+ "scripts/lib/manifest-core.mjs"
29
41
  ]
30
42
  }
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chmod, cp, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const MCP_BUNDLES = [
8
+ ["navigator", "code-nav-mcp.bundle.min.mjs", ["code-nav-mcp.bundle.min.mjs", "code-nav.bundle.min.mjs"], []],
9
+ ["redskilled", "redskilled-mcp.bundle.min.mjs", ["redskilled-mcp.bundle.min.mjs"], []],
10
+ ["rsp", "rsp.bundle.min.mjs", ["rsp.bundle.min.mjs"], ["mcp"]],
11
+ ];
12
+
13
+ function parseArgs(argv) {
14
+ const args = { root: process.cwd(), output: "" };
15
+ for (let index = 0; index < argv.length; index += 1) {
16
+ const arg = argv[index];
17
+ if (arg === "--root" || arg === "--output") {
18
+ const value = argv[index + 1];
19
+ if (!value) throw new Error(`${arg} requires a path`);
20
+ args[arg.slice(2)] = value;
21
+ index += 1;
22
+ continue;
23
+ }
24
+ throw new Error(`unknown argument: ${arg}`);
25
+ }
26
+ if (!args.output) throw new Error("--output requires a path");
27
+ return args;
28
+ }
29
+
30
+ async function readJson(path) {
31
+ try {
32
+ return JSON.parse(await readFile(path, "utf8"));
33
+ } catch (error) {
34
+ throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`);
35
+ }
36
+ }
37
+
38
+ async function requireFile(path, label) {
39
+ let metadata;
40
+ try {
41
+ metadata = await stat(path);
42
+ } catch {
43
+ throw new Error(`${label} not found: ${path}`);
44
+ }
45
+ if (!metadata.isFile()) throw new Error(`${label} is not a file: ${path}`);
46
+ }
47
+
48
+ function within(root, path) {
49
+ const rel = relative(root, path);
50
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
51
+ }
52
+
53
+ function extensionPath(...parts) {
54
+ return ["${extensionPath}", ...parts].join("${/}");
55
+ }
56
+
57
+ function jsonBytes(value) {
58
+ return `${JSON.stringify(value, null, 2)}\n`;
59
+ }
60
+
61
+ export async function buildGeminiExtension({ root, output }) {
62
+ const sourceRoot = await realpath(resolve(root));
63
+ const pluginRoot = join(sourceRoot, "plugins", "dev");
64
+ const claudeManifestPath = join(pluginRoot, ".claude-plugin", "plugin.json");
65
+ const claude = await readJson(claudeManifestPath);
66
+ if (claude.name !== "dev") throw new Error(`${claudeManifestPath}: name must be dev`);
67
+ if (typeof claude.version !== "string" || !claude.version) {
68
+ throw new Error(`${claudeManifestPath}: version must be a non-empty string`);
69
+ }
70
+ if (typeof claude.description !== "string" || !claude.description) {
71
+ throw new Error(`${claudeManifestPath}: description must be a non-empty string`);
72
+ }
73
+ if (!Array.isArray(claude.skills) || claude.skills.length === 0) {
74
+ throw new Error(`${claudeManifestPath}: skills must be a non-empty array`);
75
+ }
76
+
77
+ const target = resolve(output);
78
+ const staging = `${target}.tmp-${process.pid}`;
79
+ await rm(staging, { recursive: true, force: true });
80
+ await mkdir(join(staging, "skills"), { recursive: true });
81
+ await mkdir(join(staging, "hooks"), { recursive: true });
82
+ await mkdir(join(staging, "dist"), { recursive: true });
83
+
84
+ const skillNames = new Set();
85
+ for (const declared of claude.skills) {
86
+ if (typeof declared !== "string" || !declared.startsWith("./skills/")) {
87
+ throw new Error(`${claudeManifestPath}: invalid skill path ${JSON.stringify(declared)}`);
88
+ }
89
+ const source = resolve(pluginRoot, declared);
90
+ if (!within(pluginRoot, source)) {
91
+ throw new Error(`${claudeManifestPath}: skill escapes plugin root: ${declared}`);
92
+ }
93
+ await requireFile(join(source, "SKILL.md"), `declared skill ${declared}`);
94
+ const name = basename(source);
95
+ if (skillNames.has(name)) {
96
+ throw new Error(`${claudeManifestPath}: duplicate flattened Gemini skill name: ${name}`);
97
+ }
98
+ skillNames.add(name);
99
+ await cp(source, join(staging, "skills", name), { recursive: true, force: true });
100
+ }
101
+
102
+ const guardSource = join(pluginRoot, "hooks", "command-guard.sh");
103
+ await requireFile(guardSource, "Gemini command guard hook");
104
+ await cp(guardSource, join(staging, "hooks", "command-guard.sh"), { force: true });
105
+ await chmod(join(staging, "hooks", "command-guard.sh"), 0o755);
106
+
107
+ const mcpServers = {};
108
+ for (const [name, bundle, sourceNames, trailingArgs] of MCP_BUNDLES) {
109
+ let source = "";
110
+ for (const sourceName of sourceNames) {
111
+ const candidate = join(sourceRoot, "dist", sourceName);
112
+ try {
113
+ await requireFile(candidate, `Gemini ${name} MCP bundle`);
114
+ source = candidate;
115
+ break;
116
+ } catch {
117
+ // Try the next canonical package-set spelling.
118
+ }
119
+ }
120
+ if (!source) {
121
+ throw new Error(`Gemini ${name} MCP bundle not found: ${sourceNames.join(", ")}`);
122
+ }
123
+ await cp(source, join(staging, "dist", bundle), { force: true });
124
+ mcpServers[name] = {
125
+ command: "node",
126
+ args: [extensionPath("dist", bundle), ...trailingArgs],
127
+ cwd: "${extensionPath}",
128
+ };
129
+ }
130
+
131
+ await writeFile(
132
+ join(staging, "gemini-extension.json"),
133
+ jsonBytes({
134
+ name: "dev",
135
+ version: claude.version,
136
+ description: String(claude.description)
137
+ .replace(/[‘’`]/g, "")
138
+ .replace(/[“”]/g, '"')
139
+ .replace(/[–—]/g, "-")
140
+ .replace(/…/g, "...")
141
+ .replace(/\s+/g, " ")
142
+ .trim(),
143
+ mcpServers,
144
+ }),
145
+ );
146
+ await writeFile(
147
+ join(staging, "hooks", "hooks.json"),
148
+ jsonBytes({
149
+ hooks: {
150
+ BeforeTool: [
151
+ {
152
+ matcher: "run_shell_command",
153
+ hooks: [
154
+ {
155
+ type: "command",
156
+ command: extensionPath("hooks", "command-guard.sh"),
157
+ description: "Enforce RedSkills command and worktree guardrails",
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ },
163
+ }),
164
+ );
165
+
166
+ await mkdir(dirname(target), { recursive: true });
167
+ await rm(target, { recursive: true, force: true });
168
+ await rename(staging, target);
169
+ }
170
+
171
+ async function main() {
172
+ await buildGeminiExtension(parseArgs(process.argv.slice(2)));
173
+ }
174
+
175
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
176
+ main().catch((error) => {
177
+ console.error(error instanceof Error ? error.message : String(error));
178
+ process.exitCode = 1;
179
+ });
180
+ }
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ jsonBytes,
7
+ normalizeSkillEntry,
8
+ normalizeText,
9
+ parseArgs,
10
+ printDiffs,
11
+ readJson,
12
+ titleCaseName,
13
+ writeGenerated,
14
+ } from "./lib/manifest-core.mjs";
15
+
16
+ const REPO_URL = "https://github.com/reddb-io/red-skills";
17
+ const AUTHOR = {
18
+ name: "reddb.io",
19
+ url: "https://github.com/reddb-io",
20
+ };
21
+ const CAPABILITIES = ["Interactive", "Read", "Write", "Shell"];
22
+ const PREFERRED_SKILL_ROOT_ORDER = ["engineering", "knowledge", "productivity", "misc", "core"];
23
+
24
+ function marketplaceDisplayName(name) {
25
+ if (name === "red-skills") return "RedSkills";
26
+ return titleCaseName(name);
27
+ }
28
+
29
+ function brandColorForPlugin(name) {
30
+ if (name === "brain") return "#2563EB";
31
+ return "#D92D20";
32
+ }
33
+
34
+ function pluginKeywords(name) {
35
+ const keywords = ["codex", "claude-code", "skills", "agents"];
36
+ if (name === "dev") return [...keywords, "github-issues", "tdd"];
37
+ return [...keywords, name];
38
+ }
39
+
40
+ function deriveCodexSkillRoots(skills) {
41
+ if (typeof skills === "string") {
42
+ return skills.endsWith("/") ? skills : `${skills}/`;
43
+ }
44
+ if (!Array.isArray(skills)) return undefined;
45
+
46
+ const roots = new Set();
47
+ for (const skill of skills) {
48
+ const normalized = normalizeSkillEntry(skill);
49
+ const match = normalized.match(/^\.\/skills\/([^/]+)/);
50
+ if (match) roots.add(match[1]);
51
+ }
52
+
53
+ const orderedRoots = [...roots].sort((left, right) => {
54
+ const leftIndex = PREFERRED_SKILL_ROOT_ORDER.indexOf(left);
55
+ const rightIndex = PREFERRED_SKILL_ROOT_ORDER.indexOf(right);
56
+ if (leftIndex !== -1 || rightIndex !== -1) {
57
+ return (leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex);
58
+ }
59
+ return left.localeCompare(right);
60
+ });
61
+
62
+ return orderedRoots.map((root) => `./skills/${root}/`);
63
+ }
64
+
65
+ function codexHooksPath(hooks) {
66
+ if (typeof hooks !== "string") return undefined;
67
+ return hooks.replace(/claude\.hooks\.json$/, "codex.hooks.json");
68
+ }
69
+
70
+ function defaultPromptForPlugin(name, skills) {
71
+ const skillEntries = Array.isArray(skills) ? skills : [];
72
+ const skillNames = skillEntries
73
+ .map((entry) => normalizeSkillEntry(entry).split("/").at(-1))
74
+ .filter(Boolean)
75
+ .slice(0, 4);
76
+
77
+ if (skillNames.length === 0) return [`Use RedSkills ${titleCaseName(name)} when this workspace needs it.`];
78
+ return skillNames.map((skill) => `Use $${skill} when this workspace needs it.`);
79
+ }
80
+
81
+ function maybeSet(target, key, value) {
82
+ if (value !== undefined) target[key] = value;
83
+ }
84
+
85
+ function parseYamlScalar(value, field, relativePath) {
86
+ if (value.startsWith('"')) {
87
+ try {
88
+ return JSON.parse(value);
89
+ } catch {
90
+ throw new Error(`${relativePath}: ${field} is not a valid quoted scalar`);
91
+ }
92
+ }
93
+ if (value.startsWith("'") && value.endsWith("'")) {
94
+ return value.slice(1, -1).replace(/''/g, "'");
95
+ }
96
+ return value;
97
+ }
98
+
99
+ /** Parse the three SKILL.md frontmatter fields projected into Codex's sidecar. */
100
+ export function parseCodexSkillFrontmatter(source, relativePath = "SKILL.md") {
101
+ const lines = source.split(/\r?\n/);
102
+ if (lines[0] !== "---") throw new Error(`${relativePath}: missing YAML frontmatter`);
103
+ const end = lines.indexOf("---", 1);
104
+ if (end === -1) throw new Error(`${relativePath}: unterminated YAML frontmatter`);
105
+
106
+ const values = new Map();
107
+ for (let index = 1; index < end; index += 1) {
108
+ const match = /^([a-z][a-z0-9-]*):(?:\s*(.*))?$/.exec(lines[index]);
109
+ if (!match) continue;
110
+ const [, key, rawValue = ""] = match;
111
+ if (rawValue === ">-" || rawValue === ">" || rawValue === "|-" || rawValue === "|") {
112
+ const continuation = [];
113
+ while (index + 1 < end && /^(?:\s+|$)/.test(lines[index + 1])) {
114
+ index += 1;
115
+ continuation.push(lines[index].trim());
116
+ }
117
+ values.set(key, continuation.join(rawValue.startsWith(">") ? " " : "\n").trim());
118
+ continue;
119
+ }
120
+ values.set(key, parseYamlScalar(rawValue.trim(), key, relativePath));
121
+ }
122
+
123
+ const name = values.get("name");
124
+ const description = values.get("description");
125
+ if (typeof name !== "string" || name.trim() === "") {
126
+ throw new Error(`${relativePath}: frontmatter has no name`);
127
+ }
128
+ if (typeof description !== "string" || description.trim() === "") {
129
+ throw new Error(`${relativePath}: frontmatter has no description`);
130
+ }
131
+ return {
132
+ name,
133
+ description,
134
+ disableModelInvocation: values.get("disable-model-invocation") === "true",
135
+ };
136
+ }
137
+
138
+ /** Codex accepts JSON string scalars in YAML, keeping escaping dependency-free. */
139
+ export function buildCodexSkillSidecar(frontmatter) {
140
+ const lines = [
141
+ "interface:",
142
+ ` display_name: ${JSON.stringify(titleCaseName(frontmatter.name))}`,
143
+ ` short_description: ${JSON.stringify(normalizeText(frontmatter.description))}`,
144
+ ];
145
+ if (frontmatter.disableModelInvocation) {
146
+ lines.push("policy:", " allow_implicit_invocation: false");
147
+ }
148
+ return `${lines.join("\n")}\n`;
149
+ }
150
+
151
+ async function walkSkillFiles(directory) {
152
+ let entries;
153
+ try {
154
+ entries = await readdir(directory, { withFileTypes: true });
155
+ } catch (error) {
156
+ if (error && error.code === "ENOENT") return [];
157
+ throw error;
158
+ }
159
+
160
+ const files = [];
161
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
162
+ const path = join(directory, entry.name);
163
+ if (entry.isDirectory()) files.push(...await walkSkillFiles(path));
164
+ else if (entry.isFile() && entry.name === "SKILL.md") files.push(path);
165
+ }
166
+ return files;
167
+ }
168
+
169
+ /** Derive the sidecar obligation from every plugin's skills tree, never a hand-kept list. */
170
+ export async function discoverCodexSkillFiles(root) {
171
+ const pluginsRoot = join(root, "plugins");
172
+ let plugins;
173
+ try {
174
+ plugins = await readdir(pluginsRoot, { withFileTypes: true });
175
+ } catch (error) {
176
+ if (error && error.code === "ENOENT") return [];
177
+ throw error;
178
+ }
179
+
180
+ const files = [];
181
+ for (const plugin of plugins.sort((left, right) => left.name.localeCompare(right.name))) {
182
+ if (!plugin.isDirectory()) continue;
183
+ files.push(...await walkSkillFiles(join(pluginsRoot, plugin.name, "skills")));
184
+ }
185
+ return files;
186
+ }
187
+
188
+ export function buildCodexMarketplace(claudeMarketplace) {
189
+ return {
190
+ name: claudeMarketplace.name,
191
+ interface: {
192
+ displayName: marketplaceDisplayName(claudeMarketplace.name),
193
+ },
194
+ plugins: claudeMarketplace.plugins.map((plugin) => {
195
+ const codexPlugin = {
196
+ name: plugin.name,
197
+ source: {
198
+ source: "local",
199
+ path: plugin.source,
200
+ },
201
+ policy: {
202
+ installation: plugin.name === "dev" ? "INSTALLED_BY_DEFAULT" : "AVAILABLE",
203
+ authentication: "ON_USE",
204
+ },
205
+ };
206
+ maybeSet(codexPlugin, "dependencies", plugin.dependencies);
207
+ maybeSet(codexPlugin, "description", normalizeText(plugin.description));
208
+ codexPlugin.category = "Developer Tools";
209
+ return codexPlugin;
210
+ }),
211
+ };
212
+ }
213
+
214
+ export function buildCodexPluginManifest(claudePlugin) {
215
+ const description = normalizeText(claudePlugin.description);
216
+ const manifest = {
217
+ name: claudePlugin.name,
218
+ version: claudePlugin.version,
219
+ description,
220
+ author: AUTHOR,
221
+ homepage: REPO_URL,
222
+ repository: REPO_URL,
223
+ license: "Apache-2.0",
224
+ };
225
+
226
+ maybeSet(manifest, "dependencies", claudePlugin.dependencies);
227
+ manifest.keywords = pluginKeywords(claudePlugin.name);
228
+ maybeSet(manifest, "skills", deriveCodexSkillRoots(claudePlugin.skills));
229
+ maybeSet(manifest, "hooks", codexHooksPath(claudePlugin.hooks));
230
+ maybeSet(manifest, "mcpServers", claudePlugin.mcpServers);
231
+ manifest.interface = {
232
+ displayName: `RedSkills ${titleCaseName(claudePlugin.name)}`,
233
+ shortDescription: description,
234
+ longDescription: description,
235
+ developerName: AUTHOR.name,
236
+ category: "Developer Tools",
237
+ capabilities: CAPABILITIES,
238
+ websiteURL: REPO_URL,
239
+ defaultPrompt: defaultPromptForPlugin(claudePlugin.name, claudePlugin.skills),
240
+ brandColor: brandColorForPlugin(claudePlugin.name),
241
+ };
242
+
243
+ return manifest;
244
+ }
245
+
246
+ export async function generateCodexManifests({ root, check = false }) {
247
+ const mismatches = [];
248
+ const claudeMarketplacePath = join(root, ".claude-plugin/marketplace.json");
249
+ const codexMarketplacePath = join(root, ".agents/plugins/marketplace.json");
250
+ const claudeMarketplace = await readJson(claudeMarketplacePath);
251
+ await writeGenerated(codexMarketplacePath, jsonBytes(buildCodexMarketplace(claudeMarketplace)), check, mismatches);
252
+
253
+ for (const plugin of claudeMarketplace.plugins) {
254
+ const pluginRoot = join(root, plugin.source);
255
+ const claudePlugin = await readJson(join(pluginRoot, ".claude-plugin/plugin.json"));
256
+ await writeGenerated(
257
+ join(pluginRoot, ".codex-plugin/plugin.json"),
258
+ jsonBytes(buildCodexPluginManifest(claudePlugin)),
259
+ check,
260
+ mismatches,
261
+ );
262
+ }
263
+
264
+ for (const skillPath of await discoverCodexSkillFiles(root)) {
265
+ const relativePath = relative(root, skillPath).replaceAll("\\", "/");
266
+ const frontmatter = parseCodexSkillFrontmatter(await readFile(skillPath, "utf8"), relativePath);
267
+ await writeGenerated(
268
+ join(dirname(skillPath), "agents/openai.yaml"),
269
+ buildCodexSkillSidecar(frontmatter),
270
+ check,
271
+ mismatches,
272
+ );
273
+ }
274
+
275
+ if (check && mismatches.length > 0) {
276
+ await printDiffs(root, mismatches, { tempLabel: "red-skills-codex-manifest-diff-" });
277
+ throw new Error(`Codex manifests are stale; run node scripts/generate-codex-manifests.mjs (${mismatches.length} file(s))`);
278
+ }
279
+ }
280
+
281
+ async function main() {
282
+ const args = parseArgs(process.argv.slice(2));
283
+ await generateCodexManifests(args);
284
+ }
285
+
286
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
287
+ main().catch((error) => {
288
+ console.error(error instanceof Error ? error.message : String(error));
289
+ process.exitCode = 1;
290
+ });
291
+ }