feat: initialize OmniClaw skills registry

This commit is contained in:
boris
2026-04-24 01:12:20 -07:00
commit 789bb38e69
16 changed files with 1713 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.DS_Store
._*
node_modules/
.env
.env.*
!.env.example
*.log
tmp/

8
AGENTS.md Normal file
View File

@@ -0,0 +1,8 @@
# Repository Instructions
- Keep skills self-contained and installable by copying one folder into `$CODEX_HOME/skills`.
- Do not commit real credentials, API keys, OAuth tokens, generated images, or private logs.
- Run `node tools/validate.mjs` before committing.
- Run `node tools/build-registry.mjs` after changing any skill metadata, skill file, reference, script, or API document.
- Commit `registry/manifest.json` and `packages/*.tar.gz` because public installers and website pages may consume them directly.

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# OmniClaw Skills Registry
OmniClaw Skills Registry is the source repository for reusable AI-agent skills and API operation guides published by OmniClaw.
It is designed for three consumers:
- Humans browsing API documentation and skill usage guides.
- Codex or other AI agents installing skills into their local skill directory.
- The OmniClaw release website rendering a searchable public catalog from `registry/manifest.json`.
## Repository Layout
```text
skills/
<skill-name>/
SKILL.md
agents/openai.yaml
references/
scripts/
skill.registry.json
apis/
<product>/
<api>.zh.md
<api>.en.md
registry/
manifest.schema.json
manifest.json
packages/
<skill-name>-<version>.tar.gz
tools/
build-registry.mjs
validate.mjs
install.sh
```
## Install A Skill
Default install target is `${CODEX_HOME:-$HOME/.codex}/skills`.
```bash
curl -fsSL https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/tools/install.sh \
| bash -s -- sub2api-gpt-image-2
```
Alternative with git clone:
```bash
git clone https://git.omniclaw.store/zsb/omniclaw-skills.git
cd omniclaw-skills
./tools/install.sh sub2api-gpt-image-2
```
Restart Codex after installing new skills.
## Current Skills
- `sub2api-gpt-image-2`: Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible APIs.
## Development
Validate everything:
```bash
node tools/validate.mjs
```
Rebuild manifest and skill packages:
```bash
node tools/build-registry.mjs
```
The build script writes:
- `registry/manifest.json`
- `packages/<skill-name>-<version>.tar.gz`
Commit generated registry and packages so the release website and remote installers can consume stable artifacts directly.
## Publishing Contract
Every skill must include:
- `SKILL.md`
- `skill.registry.json`
- `agents/openai.yaml`
Every API guide should provide both:
- `*.zh.md`
- `*.en.md`
Do not commit real API keys, OAuth tokens, generated images, or private runtime logs.

View File

@@ -0,0 +1,370 @@
# GPT Image 2 API Guide
This guide describes how to call `gpt-image-2` through sub2api or any OpenAI-compatible gateway.
Default examples use:
```text
BASE_URL=https://claude.omniclaw.store/v1
API_KEY=<sub2api API key generated from the /keys page>
```
Do not use ChatGPT OAuth tokens from `.codex/auth.json` as API keys.
## Quick Summary
- Direct image generation: call `POST /v1/images/generations` with `model: "gpt-image-2"`.
- Image editing: call `POST /v1/images/edits` with multipart `image[]` files and an optional `mask`.
- Agent/Codex workflows: keep the main model as a text/agent model such as `gpt-5.5`, then call image generation through the Responses API `image_generation` tool.
- Do not use `gpt-image-2` as the Codex main model.
- `gpt-image-2` normally returns base64 image data at `data[0].b64_json`.
- `3840x2160` 4K output works but is high-latency and high-cost; use 180-300 second timeouts for production.
## Official Capability Summary
`gpt-image-2` is an image generation and editing model with text input, image input, and image output support.
Model aliases:
```text
gpt-image-2
gpt-image-2-2026-04-21
```
Supported API surfaces:
```text
/v1/images/generations
/v1/images/edits
/v1/responses # via image_generation tool
```
Official references:
- https://developers.openai.com/api/docs/models/gpt-image-2
- https://developers.openai.com/api/docs/guides/image-generation
- https://developers.openai.com/api/reference/resources/images
## Authentication
```bash
export BASE_URL="https://claude.omniclaw.store/v1"
export API_KEY="sk-..."
```
JSON requests require:
```http
Authorization: Bearer $API_KEY
Content-Type: application/json
```
For multipart image edits, let `curl -F` or the SDK set `Content-Type`.
## Image Generation
### Minimal Request
```bash
curl -sS "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "A compact Apple-style dashboard UI, clean white background",
"size": "1024x1024",
"quality": "medium",
"output_format": "png",
"n": 1
}' > image.json
```
Decode the response:
```bash
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
```
### 4K Request
```bash
curl -sS "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
--max-time 300 \
-d '{
"model": "gpt-image-2",
"prompt": "A modern product poster, cinematic lighting, premium realistic photography",
"size": "3840x2160",
"quality": "medium",
"output_format": "png",
"n": 1
}' > image-4k.json
```
Production recommendation: first validate prompts with `1024x1024` or `1536x1024`, then upscale the request to `3840x2160`. `4K + high` can be slow and expensive.
## Generation Parameters
| Parameter | Type | Recommended value | Notes |
|---|---|---|---|
| `model` | string | `gpt-image-2` | Required. The snapshot `gpt-image-2-2026-04-21` is also valid. |
| `prompt` | string | detailed natural language | Required. Include subject, environment, camera, style, lighting, and constraints. |
| `n` | number | `1` | Number of images. Prefer single-image requests for retry and billing attribution. |
| `size` | string | `1024x1024`, `1536x1024`, `3840x2160` | Flexible sizes are supported when they satisfy the model constraints. |
| `quality` | string | `low`, `medium`, `high`, `auto` | Use `low` for drafts, `medium` for normal output, `high` for final assets. |
| `output_format` | string | `png`, `jpeg`, `webp` | Default is usually `png`; use `jpeg` for latency-sensitive outputs. |
| `output_compression` | number | `0-100` | Only applies to `jpeg` and `webp`. |
| `background` | string | `auto`, `opaque` | `gpt-image-2` currently does not support `transparent`. |
| `moderation` | string | `auto`, `low` | Adjusts filtering level but does not bypass safety policy. |
| `stream` | boolean | `false` | Enables SSE image streaming. |
| `partial_images` | number | `0-3` | Streaming only; partial images increase output token cost. |
| `user` | string | end-user ID | Useful for audit and abuse monitoring. |
## Size Constraints
`size` can be `auto` or a valid `widthxheight` value:
- Maximum edge length is `3840px`.
- Width and height must both be multiples of `16px`.
- Long edge to short edge ratio must be at most `3:1`.
- Total pixels must be between `655,360` and `8,294,400`.
Common values:
```text
1024x1024
1536x1024
1024x1536
2048x2048
2048x1152
3840x2160
2160x3840
auto
```
Treat outputs larger than `2560x1440` as experimental high-pixel workloads with higher latency, higher cost, and higher failure probability.
## Response Shape
Typical response:
```json
{
"created": 1770000000,
"background": "opaque",
"data": [
{
"b64_json": "...",
"revised_prompt": "..."
}
],
"model": "gpt-image-2",
"output_format": "png",
"quality": "medium",
"size": "1024x1024",
"usage": {
"input_tokens": 43,
"input_tokens_details": {
"image_tokens": 0,
"text_tokens": 43
},
"output_tokens": 196,
"output_tokens_details": {
"image_tokens": 196,
"text_tokens": 0
},
"total_tokens": 239
}
}
```
Production systems should store:
- `model`
- `size`
- `quality`
- `output_format`
- `usage.total_tokens`
- `usage.input_tokens`
- `usage.output_tokens`
- latency
- upstream account, group, user, and key identifiers
## Image Editing
### Single-image Edit
```bash
curl -sS "$BASE_URL/images/edits" \
-H "Authorization: Bearer $API_KEY" \
-F "model=gpt-image-2" \
-F "image[]=@input.png" \
-F "prompt=Replace the sofa with a minimalist white lounge chair" \
-F "size=1024x1024" \
-F "quality=medium" \
-F "output_format=png" \
> edit.json
```
### Masked Local Edit
```bash
curl -sS "$BASE_URL/images/edits" \
-H "Authorization: Bearer $API_KEY" \
-F "model=gpt-image-2" \
-F "image[]=@input.png" \
-F "mask=@mask.png" \
-F "prompt=Change only the transparent masked region into a glass button" \
-F "size=1024x1024" \
-F "quality=medium" \
> edit-mask.json
```
Mask requirements:
- `image` and `mask` must have the same format and dimensions.
- Files must be under 50MB.
- `mask` must include an alpha channel.
- Do not pass `input_fidelity` for `gpt-image-2`; the model processes image inputs at high fidelity by default.
## Responses API With `image_generation`
Use this when an agent should reason about the task before generating an image. The main model should be a text/agent model, such as `gpt-5.5`.
```bash
curl -sS "$BASE_URL/responses" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"input": "Generate a clean product poster for an AI proxy service.",
"tools": [
{
"type": "image_generation",
"quality": "medium",
"size": "1536x1024",
"output_format": "png"
}
]
}' > response-image.json
```
Important:
- `model` is the main reasoning model, not `gpt-image-2`.
- The `image_generation` tool performs the image work.
- sub2api may inject the image tool for official Codex clients, but application calls should pass it explicitly.
## Streaming Images
The Images API supports SSE streaming:
```bash
curl -N "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "A futuristic city skyline at sunrise",
"stream": true,
"partial_images": 2,
"size": "1536x1024",
"quality": "medium"
}'
```
Events:
```text
image_generation.partial_image
image_generation.completed
```
`partial_images` can be `0-3`. Each partial image adds output token cost.
## SDK Examples
### Node.js
```ts
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: process.env.BASE_URL ?? "https://claude.omniclaw.store/v1",
});
const result = await client.images.generate({
model: "gpt-image-2",
prompt: "A premium product poster for an AI service",
size: "1536x1024",
quality: "medium",
output_format: "png",
n: 1,
});
const b64 = result.data?.[0]?.b64_json;
if (!b64) throw new Error("No image returned");
fs.writeFileSync("image.png", Buffer.from(b64, "base64"));
```
### Python
```py
import base64
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["API_KEY"],
base_url=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"),
)
result = client.images.generate(
model="gpt-image-2",
prompt="A premium product poster for an AI service",
size="1536x1024",
quality="medium",
output_format="png",
n=1,
)
b64 = result.data[0].b64_json
with open("image.png", "wb") as f:
f.write(base64.b64decode(b64))
```
## Production Dispatch
- Routing: prefer plus/team/pro OpenAI OAuth accounts for image workloads.
- Timeout: use 120 seconds for normal images and 300 seconds for 4K.
- Retry: only retry transient network failures and 502/503/504 with low retry counts.
- Concurrency: 4K output produces many image tokens; use low per-account concurrency. Standard 1024 images can use higher concurrency.
- Billing: record `usage` and charge based on input and output tokens. 4K can produce far more output tokens than 1024 images.
- Latency: use `jpeg` and `quality: low` for drafts or latency-sensitive previews.
- Fallback: if `4K/high` fails, retry `4K/medium`; if that still fails, generate `1536x1024/medium` and upscale separately.
## Common Errors
| Symptom | Likely cause | Action |
|---|---|---|
| `401 INVALID_API_KEY` | Key is not a sub2api key or is disabled/deleted | Generate a new key from `/keys` |
| `400 invalid_request_error` | Incompatible params such as transparent background or invalid size | Check `size`, `background`, and `quality` |
| `429 usage_limit_reached` | Upstream account usage window hit | Switch plus/team/pro account or wait for reset |
| `502 Upstream request failed` | Upstream did not return image data, network failed, or content was refused | Inspect server logs, simplify prompt, lower quality or size |
| Request takes over 2 minutes | High pixels or complex prompt | Increase timeout, use streaming, or test lower resolution first |
| `/v1/models` does not show `gpt-image-2` | Codex/text model list is not the Images API capability list | Call `/v1/images/generations` directly |
## Safety Boundary
Filter clearly disallowed content before sending requests, especially:
- Sexualized minors or young-looking subjects
- Non-consensual sexual content, coercion, or sexual violence
- Explicit nudity or graphic sexual activity
- Illegal, hateful, or extreme violent content
For safe romantic scenes, explicitly constrain prompts with terms such as adult, non-explicit, no nudity, and fully clothed.

View File

@@ -0,0 +1,365 @@
# GPT Image 2 API 调用文档
本文档面向 sub2api/OpenAI-compatible 网关调用 `gpt-image-2`。示例默认使用:
```text
BASE_URL=https://claude.omniclaw.store/v1
API_KEY=<从 /keys 页面生成的 sub2api key>
```
不要把 `.codex/auth.json` 里的 ChatGPT OAuth token 当 API key 使用。
## 快速结论
- 直接生成图片:使用 `POST /v1/images/generations``model``gpt-image-2`
- 编辑图片:使用 `POST /v1/images/edits`multipart 上传 `image[]`、可选 `mask`
- Agent/Codex 场景:主模型仍用 `gpt-5.5`,通过 Responses API 的 `image_generation` tool 调图像能力;不要把 Codex 主模型设成 `gpt-image-2`
- `gpt-image-2` 返回 base64 图片数据,通常是 `data[0].b64_json`
- `3840x2160` 4K 可用,但属于高像素、长耗时场景;生产调用应设置 180-300 秒超时。
## 官方能力摘要
`gpt-image-2` 是图片生成和编辑模型,支持文本输入、图片输入、图片输出。模型别名和快照:
```text
gpt-image-2
gpt-image-2-2026-04-21
```
支持端点:
```text
/v1/images/generations
/v1/images/edits
/v1/responses # 通过 image_generation tool
```
官方参考:
- https://developers.openai.com/api/docs/models/gpt-image-2
- https://developers.openai.com/api/docs/guides/image-generation
- https://developers.openai.com/api/reference/resources/images
## 认证
```bash
export BASE_URL="https://claude.omniclaw.store/v1"
export API_KEY="sk-..."
```
所有 JSON 请求带:
```http
Authorization: Bearer $API_KEY
Content-Type: application/json
```
multipart 编辑接口由 `curl -F` 或 SDK 自动设置 `Content-Type`
## 生成图片
### 最小请求
```bash
curl -sS "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "A compact Apple-style dashboard UI, clean white background",
"size": "1024x1024",
"quality": "medium",
"output_format": "png",
"n": 1
}' > image.json
```
解码:
```bash
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
```
### 4K 请求
```bash
curl -sS "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
--max-time 300 \
-d '{
"model": "gpt-image-2",
"prompt": "A modern product poster, cinematic lighting, premium realistic photography",
"size": "3840x2160",
"quality": "medium",
"output_format": "png",
"n": 1
}' > image-4k.json
```
生产建议4K + `high` 很慢且成本高。先用 `1024x1024``1536x1024` 验证提示词,再升到 `3840x2160`
## 生成参数
| 参数 | 类型 | 建议值 | 说明 |
|---|---|---|---|
| `model` | string | `gpt-image-2` | 必填。也可用快照 `gpt-image-2-2026-04-21`。 |
| `prompt` | string | 详细自然语言 | 必填。写清主体、环境、镜头、风格、限制。 |
| `n` | number | `1` | 生成数量。生产建议单张并发调度,便于重试和计费。 |
| `size` | string | `1024x1024``1536x1024``3840x2160` | `gpt-image-2` 支持灵活尺寸,见下方尺寸约束。 |
| `quality` | string | `low``medium``high``auto` | 草稿用 `low`,常规用 `medium`,最终图用 `high`。 |
| `output_format` | string | `png``jpeg``webp` | 默认 `png`。延迟敏感优先 `jpeg`。 |
| `output_compression` | number | `0-100` | 仅 `jpeg`/`webp` 有意义。 |
| `background` | string | `auto``opaque` | `gpt-image-2` 当前不支持 `transparent`。 |
| `moderation` | string | `auto``low` | 控制图像生成过滤强度;仍需遵守内容政策。 |
| `stream` | boolean | `false` | 开启 SSE 流式图片事件。 |
| `partial_images` | number | `0-3` | 流式时返回部分图片;会增加输出 token 成本。 |
| `user` | string | 用户 ID | 终端用户标识,便于审计和滥用监控。 |
## 尺寸约束
`gpt-image-2``size` 可以是 `auto`,也可以是满足约束的 `宽x高`
- 最大边不超过 `3840px`
- 宽和高都必须是 `16px` 的倍数
- 长边/短边比例不超过 `3:1`
- 总像素在 `655,360``8,294,400` 之间
常用尺寸:
```text
1024x1024 # 方图,通常最快
1536x1024 # 横图
1024x1536 # 竖图
2048x2048 # 2K 方图
2048x1152 # 2K 横图
3840x2160 # 4K 横图
2160x3840 # 4K 竖图
auto
```
超过 `2560x1440` 的输出通常应按实验性高像素场景处理:高延迟、高成本、失败概率更高。
## 返回结构
典型响应:
```json
{
"created": 1770000000,
"background": "opaque",
"data": [
{
"b64_json": "...",
"revised_prompt": "..."
}
],
"model": "gpt-image-2",
"output_format": "png",
"quality": "medium",
"size": "1024x1024",
"usage": {
"input_tokens": 43,
"input_tokens_details": {
"image_tokens": 0,
"text_tokens": 43
},
"output_tokens": 196,
"output_tokens_details": {
"image_tokens": 196,
"text_tokens": 0
},
"total_tokens": 239
}
}
```
业务侧应持久化:
- `model`
- `size`
- `quality`
- `output_format`
- `usage.total_tokens`
- `usage.input_tokens`
- `usage.output_tokens`
- 请求耗时
- 上游账号/分组/用户/key
## 编辑图片
### 单图编辑
```bash
curl -sS "$BASE_URL/images/edits" \
-H "Authorization: Bearer $API_KEY" \
-F "model=gpt-image-2" \
-F "image[]=@input.png" \
-F "prompt=Replace the sofa with a minimalist white lounge chair" \
-F "size=1024x1024" \
-F "quality=medium" \
-F "output_format=png" \
> edit.json
```
### 局部遮罩编辑
```bash
curl -sS "$BASE_URL/images/edits" \
-H "Authorization: Bearer $API_KEY" \
-F "model=gpt-image-2" \
-F "image[]=@input.png" \
-F "mask=@mask.png" \
-F "prompt=Change only the transparent masked region into a glass button" \
-F "size=1024x1024" \
-F "quality=medium" \
> edit-mask.json
```
遮罩要求:
- `image``mask` 必须同格式、同尺寸
- 文件小于 50MB
- `mask` 必须包含 alpha 通道
- `gpt-image-2` 不要传 `input_fidelity`;它自动按高保真处理输入图
## Responses API 调用 image_generation tool
用于多轮 Agent、让模型先理解需求再调用图片工具。主模型使用文本/Agent 模型,例如 `gpt-5.5`
```bash
curl -sS "$BASE_URL/responses" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"input": "Generate a clean product poster for an AI proxy service.",
"tools": [
{
"type": "image_generation",
"quality": "medium",
"size": "1536x1024",
"output_format": "png"
}
]
}' > response-image.json
```
注意:
- `model` 是主推理模型,不是 `gpt-image-2`
- `image_generation` 工具负责图片生成
- sub2api 对 Codex 官方客户端请求会注入 `image_generation` 工具提示,但业务调用仍建议显式传 tool
## 流式图片
Image API 支持流式生成:
```bash
curl -N "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "A futuristic city skyline at sunrise",
"stream": true,
"partial_images": 2,
"size": "1536x1024",
"quality": "medium"
}'
```
事件类型:
```text
image_generation.partial_image
image_generation.completed
```
`partial_images` 可设 `0-3`。每张 partial image 会额外产生输出 token 成本。
## SDK 示例
### Node.js
```ts
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: process.env.BASE_URL ?? "https://claude.omniclaw.store/v1",
});
const result = await client.images.generate({
model: "gpt-image-2",
prompt: "A premium product poster for an AI service",
size: "1536x1024",
quality: "medium",
output_format: "png",
n: 1,
});
const b64 = result.data?.[0]?.b64_json;
if (!b64) throw new Error("No image returned");
fs.writeFileSync("image.png", Buffer.from(b64, "base64"));
```
### Python
```py
import base64
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["API_KEY"],
base_url=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"),
)
result = client.images.generate(
model="gpt-image-2",
prompt="A premium product poster for an AI service",
size="1536x1024",
quality="medium",
output_format="png",
n=1,
)
b64 = result.data[0].b64_json
with open("image.png", "wb") as f:
f.write(base64.b64decode(b64))
```
## 生产调度建议
- 路由:图片生成优先使用 plus/team/pro OpenAI OAuth 账号,避免 free 账号能力不足或限流。
- 超时:普通图设置 120 秒4K 设置 300 秒。
- 重试只对网络错误、502/503/504 做有限重试;不要对内容政策拒绝无限重试。
- 并发4K 请求输出 token 高,建议单账号小并发;普通 1024 图可更高并发。
- 成本:记录 `usage` 并按 `input_tokens + output_tokens` 计费4K 输出 token 可能远高于 1024。
- 延迟:延迟敏感优先 `jpeg`,草稿用 `quality: low`
- 失败降级4K/high 失败时降为 4K/medium仍失败则 1536x1024/medium 先出图,再走放大流程。
## 常见错误
| 现象 | 可能原因 | 处理 |
|---|---|---|
| `401 INVALID_API_KEY` | key 不是 sub2api key或已删除/停用 | 从 `/keys` 重新生成 key |
| `400 invalid_request_error` | 参数不兼容,例如透明背景、尺寸不合法 | 检查 `size``background``quality` |
| `429 usage_limit_reached` | 命中 OpenAI 账号用量窗口 | 切换 plus/team/pro 账号或等待恢复 |
| `502 Upstream request failed` | 上游没返回图片、网络断开、内容被拒绝文本化 | 看服务端日志;必要时改提示词/降质量/改尺寸 |
| 超过 2 分钟 | 高像素或复杂提示词 | 设置更长超时,使用流式或先低分辨率验证 |
| `/v1/models` 不显示 `gpt-image-2` | Codex 主模型列表不等于图片接口能力列表 | 直接调用 `/v1/images/generations` |
## 安全边界
业务侧应在请求前过滤明显违规内容,尤其是:
- 未成年人或年轻化人物的性化内容
- 非自愿、胁迫、性暴力场景
- 明确裸露或露骨性行为
- 违法、仇恨、极端暴力内容
建议提示词显式写清“成年人、非露骨、无裸露、完全穿着”等约束,降低被上游拒绝或返回非图片文本的概率。

Binary file not shown.

117
registry/manifest.json Normal file
View File

@@ -0,0 +1,117 @@
{
"schema_version": "1.0.0",
"generated_at": "2026-04-24T08:11:41.942Z",
"repository": {
"name": "zsb/omniclaw-skills",
"git_url": "https://git.omniclaw.store/zsb/omniclaw-skills.git",
"raw_base_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main",
"branch": "main"
},
"skills": [
{
"name": "sub2api-gpt-image-2",
"version": "0.1.0",
"title": "Sub2API GPT Image 2",
"description": "Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible image APIs.",
"path": "skills/sub2api-gpt-image-2",
"raw_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/skills/sub2api-gpt-image-2/SKILL.md",
"package": "packages/sub2api-gpt-image-2-0.1.0.tar.gz",
"package_url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/packages/sub2api-gpt-image-2-0.1.0.tar.gz",
"sha256": "d226c0327193646879e5d9de081a9e29400efa15e194f9c5e00620bd21cceb22",
"size_bytes": 5420,
"api_docs": [
{
"id": "sub2api/gpt-image-2.zh",
"title": "GPT Image 2 API 调用文档",
"product": "sub2api",
"language": "zh",
"path": "apis/sub2api/gpt-image-2.zh.md",
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.zh.md",
"tags": [
"sub2api",
"gpt-image-2",
"zh"
],
"source_urls": [
"https://claude.omniclaw.store/v1",
"https://developers.openai.com/api/docs/models/gpt-image-2",
"https://developers.openai.com/api/docs/guides/image-generation",
"https://developers.openai.com/api/reference/resources/images"
]
},
{
"id": "sub2api/gpt-image-2.en",
"title": "GPT Image 2 API Guide",
"product": "sub2api",
"language": "en",
"path": "apis/sub2api/gpt-image-2.en.md",
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.en.md",
"tags": [
"sub2api",
"gpt-image-2",
"en"
],
"source_urls": [
"https://claude.omniclaw.store/v1",
"https://developers.openai.com/api/docs/models/gpt-image-2",
"https://developers.openai.com/api/docs/guides/image-generation",
"https://developers.openai.com/api/reference/resources/images"
]
}
],
"tags": [
"sub2api",
"openai",
"gpt-image-2",
"images",
"responses",
"codex"
],
"agents": [
"codex",
"openai-compatible-agent"
],
"updated_at": "2026-04-24"
}
],
"apis": [
{
"id": "sub2api/gpt-image-2.en",
"title": "GPT Image 2 API Guide",
"product": "sub2api",
"language": "en",
"path": "apis/sub2api/gpt-image-2.en.md",
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.en.md",
"tags": [
"sub2api",
"gpt-image-2",
"en"
],
"source_urls": [
"https://claude.omniclaw.store/v1",
"https://developers.openai.com/api/docs/models/gpt-image-2",
"https://developers.openai.com/api/docs/guides/image-generation",
"https://developers.openai.com/api/reference/resources/images"
]
},
{
"id": "sub2api/gpt-image-2.zh",
"title": "GPT Image 2 API 调用文档",
"product": "sub2api",
"language": "zh",
"path": "apis/sub2api/gpt-image-2.zh.md",
"url": "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main/apis/sub2api/gpt-image-2.zh.md",
"tags": [
"sub2api",
"gpt-image-2",
"zh"
],
"source_urls": [
"https://claude.omniclaw.store/v1",
"https://developers.openai.com/api/docs/models/gpt-image-2",
"https://developers.openai.com/api/docs/guides/image-generation",
"https://developers.openai.com/api/reference/resources/images"
]
}
]
}

View File

@@ -0,0 +1,59 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://release.omniclaw.store/skills/manifest.schema.json",
"title": "OmniClaw Skills Registry Manifest",
"type": "object",
"required": ["schema_version", "generated_at", "repository", "skills", "apis"],
"properties": {
"schema_version": { "type": "string" },
"generated_at": { "type": "string" },
"repository": {
"type": "object",
"required": ["name", "git_url", "raw_base_url", "branch"],
"properties": {
"name": { "type": "string" },
"git_url": { "type": "string" },
"raw_base_url": { "type": "string" },
"branch": { "type": "string" }
}
},
"skills": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "version", "title", "description", "path", "package", "sha256"],
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"title": { "type": "string" },
"description": { "type": "string" },
"path": { "type": "string" },
"package": { "type": "string" },
"sha256": { "type": "string" },
"size_bytes": { "type": "number" },
"api_docs": { "type": "array" },
"tags": { "type": "array", "items": { "type": "string" } },
"agents": { "type": "array", "items": { "type": "string" } },
"updated_at": { "type": "string" }
}
}
},
"apis": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title", "product", "language", "path"],
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"product": { "type": "string" },
"language": { "type": "string" },
"path": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } },
"source_urls": { "type": "array", "items": { "type": "string" } }
}
}
}
}
}

View File

@@ -0,0 +1,85 @@
---
name: sub2api-gpt-image-2
description: Use when Codex or another agent needs to call, document, troubleshoot, or generate code for GPT Image 2 through sub2api/OpenAI-compatible image APIs, including /v1/images/generations, /v1/images/edits, Responses image_generation tools, 4K image settings, streaming partial images, base64 decoding, and production error handling.
---
# Sub2API GPT Image 2
## Workflow
1. Treat `gpt-image-2` as an image generation/editing model, not a Codex or agent main model.
2. Use `/v1/images/generations` for direct text-to-image and `/v1/images/edits` for reference-image or mask edits.
3. Use `/v1/responses` with a text/agent model such as `gpt-5.5` plus an `image_generation` tool when an agent should reason before generating images.
4. Never print, commit, or hard-code real API keys. Read credentials from `API_KEY` and `BASE_URL`.
5. For detailed parameter rules, read `references/api.md`.
6. For quick non-streaming generation tests, run `scripts/generate_image.py`.
## Defaults
Use these defaults unless the task says otherwise:
```text
BASE_URL=https://claude.omniclaw.store/v1
model=gpt-image-2
size=1024x1024
quality=medium
output_format=png
n=1
timeout=300 for 4K, 120 otherwise
```
Use `3840x2160` only when explicitly requested or when a final 4K asset is needed. For prompt iteration, generate smaller images first.
## Direct Generation
Run:
```bash
export API_KEY="sk-..."
export BASE_URL="https://claude.omniclaw.store/v1"
python3 ~/.codex/skills/sub2api-gpt-image-2/scripts/generate_image.py \
--prompt "A premium product poster for an AI service" \
--size 1536x1024 \
--quality medium \
--output image.png
```
Use `--dry-run` to inspect payloads without calling the API.
## Editing
Use multipart form requests with `image[]=@file.png`; add `mask=@mask.png` only for local masked edits. For `gpt-image-2`, omit `input_fidelity` because it always processes image inputs at high fidelity.
## Responses Tool Pattern
For agentic workflows:
```json
{
"model": "gpt-5.5",
"input": "Generate a clean product poster for an AI proxy service.",
"tools": [
{
"type": "image_generation",
"size": "1536x1024",
"quality": "medium",
"output_format": "png"
}
]
}
```
Do not set the Responses `model` to `gpt-image-2`.
## Troubleshooting
- `401`: wrong or disabled sub2api key.
- `400`: invalid params such as unsupported transparent background or invalid size.
- `429`: upstream account usage limit; prefer plus/team/pro accounts.
- `502`: upstream did not return image output, network failed, or content was refused; inspect sub2api logs and simplify the prompt.
- Long latency: 4K/high can take minutes; use `jpeg`, `quality=low|medium`, or smaller sizes for drafts.
## Safety
Reject or rewrite sexualized young-looking subjects, minors, non-consensual sexual content, explicit nudity, and other disallowed content before calling the API.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Sub2API GPT Image 2"
short_description: "Generate and debug GPT Image 2 calls."
default_prompt: "Use sub2api-gpt-image-2 to generate, edit, stream, or troubleshoot GPT Image 2 API requests."

View File

@@ -0,0 +1,106 @@
# GPT Image 2 API Reference For sub2api
Source links:
- https://developers.openai.com/api/docs/models/gpt-image-2
- https://developers.openai.com/api/docs/guides/image-generation
- https://developers.openai.com/api/reference/resources/images
- Repository docs: `apis/sub2api/gpt-image-2.zh.md` and `apis/sub2api/gpt-image-2.en.md`
## Endpoints
```text
POST /v1/images/generations
POST /v1/images/edits
POST /v1/responses # image_generation tool with a text/agent model
```
`/v1/models` may only list Codex/text models in this deployment. That does not mean `/v1/images/generations` cannot accept `gpt-image-2`.
## Generation Parameters
| Parameter | Values | Notes |
|---|---|---|
| `model` | `gpt-image-2`, `gpt-image-2-2026-04-21` | Required for Images API. |
| `prompt` | string | Required. Be concrete about subject, composition, style, lighting, and safety constraints. |
| `n` | `1-10` | Prefer `1` for production retries and billing attribution. |
| `size` | `auto` or valid `WxH` | Common: `1024x1024`, `1536x1024`, `1024x1536`, `3840x2160`. |
| `quality` | `low`, `medium`, `high`, `auto` | Use `low` for drafts, `medium` for normal, `high` for final. |
| `output_format` | `png`, `jpeg`, `webp` | `jpeg` is usually faster than `png`. |
| `output_compression` | `0-100` | Applies to `jpeg` and `webp`. |
| `background` | `auto`, `opaque` | `gpt-image-2` does not support `transparent`. |
| `moderation` | `auto`, `low` | Changes filter strictness but does not bypass policy. |
| `stream` | boolean | Enables SSE image events. |
| `partial_images` | `0-3` | Streaming only; each partial image adds output token cost. |
| `user` | string | End-user identifier for abuse monitoring. |
## Size Constraints
- Maximum edge length: `3840px`.
- Both edges must be multiples of `16px`.
- Long edge to short edge ratio must be at most `3:1`.
- Total pixels must be between `655360` and `8294400`.
- Outputs larger than `2560x1440` should be treated as high-latency experimental outputs.
## Edit Notes
- Use multipart form data.
- Send reference images as `image[]`.
- Optional `mask` must match input image format and dimensions, be under 50MB, and include an alpha channel.
- Omit `input_fidelity` for `gpt-image-2`.
## Response Shape
```json
{
"data": [
{
"b64_json": "...",
"revised_prompt": "..."
}
],
"model": "gpt-image-2",
"size": "1024x1024",
"quality": "medium",
"output_format": "png",
"usage": {
"input_tokens": 43,
"output_tokens": 196,
"total_tokens": 239
}
}
```
Always decode `data[0].b64_json` unless streaming.
## Production Dispatch
- Prefer plus/team/pro upstream OpenAI OAuth accounts for image workloads.
- Set client timeout to `120s` for normal images and `300s` for 4K.
- Retry only transient network/5xx failures with low retry counts.
- Do not retry policy or validation errors without changing the prompt or params.
- Log `usage`, latency, size, quality, output format, account ID, API key ID, and request ID.
## Valid curl Template
```bash
curl -sS "$BASE_URL/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
--max-time 300 \
-d '{
"model": "gpt-image-2",
"prompt": "A premium product poster for an AI service",
"size": "1536x1024",
"quality": "medium",
"output_format": "png",
"n": 1
}' > image.json
```
Decode:
```bash
jq -r '.data[0].b64_json' image.json | base64 --decode > image.png
```

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
import argparse
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate an image with gpt-image-2 through an OpenAI-compatible /v1/images/generations endpoint."
)
parser.add_argument("--base-url", default=os.environ.get("BASE_URL", "https://claude.omniclaw.store/v1"))
parser.add_argument("--api-key-env", default="API_KEY")
parser.add_argument("--prompt", required=True)
parser.add_argument("--output", default="image.png")
parser.add_argument("--model", default="gpt-image-2")
parser.add_argument("--size", default="1024x1024")
parser.add_argument("--quality", default="medium", choices=["low", "medium", "high", "auto"])
parser.add_argument("--format", default="png", choices=["png", "jpeg", "webp"])
parser.add_argument("--compression", type=int, default=None)
parser.add_argument("--background", default=None, choices=["auto", "opaque"])
parser.add_argument("--moderation", default=None, choices=["auto", "low"])
parser.add_argument("--n", type=int, default=1)
parser.add_argument("--timeout", type=float, default=300.0)
parser.add_argument("--dry-run", action="store_true")
return parser.parse_args()
def build_payload(args: argparse.Namespace) -> dict:
payload = {
"model": args.model,
"prompt": args.prompt,
"size": args.size,
"quality": args.quality,
"output_format": args.format,
"n": args.n,
}
if args.compression is not None:
payload["output_compression"] = args.compression
if args.background:
payload["background"] = args.background
if args.moderation:
payload["moderation"] = args.moderation
return payload
def main() -> int:
args = parse_args()
payload = build_payload(args)
url = args.base_url.rstrip("/") + "/images/generations"
if args.dry_run:
print(json.dumps({"url": url, "payload": payload}, ensure_ascii=False, indent=2))
return 0
api_key = os.environ.get(args.api_key_env)
if not api_key:
print(f"Missing API key env: {args.api_key_env}", file=sys.stderr)
return 2
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
)
started = time.monotonic()
try:
with urllib.request.urlopen(req, timeout=args.timeout) as resp:
status = resp.status
raw = resp.read()
except urllib.error.HTTPError as exc:
raw = exc.read()
print(f"HTTP {exc.code}", file=sys.stderr)
print(raw.decode("utf-8", "replace")[:4000], file=sys.stderr)
return 1
except Exception as exc:
print(f"request failed: {type(exc).__name__}: {exc}", file=sys.stderr)
return 1
elapsed = time.monotonic() - started
data = json.loads(raw.decode("utf-8"))
if "error" in data:
print(json.dumps(data["error"], ensure_ascii=False), file=sys.stderr)
return 1
items = data.get("data") or []
if not items or "b64_json" not in items[0]:
print("No b64_json image returned", file=sys.stderr)
print(json.dumps(data, ensure_ascii=False)[:4000], file=sys.stderr)
return 1
image = base64.b64decode(items[0]["b64_json"])
with open(args.output, "wb") as f:
f.write(image)
summary = {
"status": status,
"elapsed_seconds": round(elapsed, 3),
"output": args.output,
"bytes": len(image),
"model": data.get("model"),
"size": data.get("size"),
"quality": data.get("quality"),
"output_format": data.get("output_format"),
"usage": data.get("usage"),
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,11 @@
{
"name": "sub2api-gpt-image-2",
"version": "0.1.0",
"title": "Sub2API GPT Image 2",
"description": "Generate, edit, stream, and troubleshoot GPT Image 2 calls through sub2api/OpenAI-compatible image APIs.",
"tags": ["sub2api", "openai", "gpt-image-2", "images", "responses", "codex"],
"agents": ["codex", "openai-compatible-agent"],
"api_docs": ["sub2api/gpt-image-2.zh", "sub2api/gpt-image-2.en"],
"updated_at": "2026-04-24"
}

157
tools/build-registry.mjs Executable file
View File

@@ -0,0 +1,157 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { createReadStream } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
const root = path.resolve(new URL("..", import.meta.url).pathname);
const skillsDir = path.join(root, "skills");
const apisDir = path.join(root, "apis");
const packagesDir = path.join(root, "packages");
const registryDir = path.join(root, "registry");
const repository = {
name: "zsb/omniclaw-skills",
git_url: "https://git.omniclaw.store/zsb/omniclaw-skills.git",
raw_base_url: "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main",
branch: "main",
};
async function exists(file) {
try {
await stat(file);
return true;
} catch {
return false;
}
}
async function sha256(file) {
return new Promise((resolve, reject) => {
const hash = createHash("sha256");
const stream = createReadStream(file);
stream.on("data", (chunk) => hash.update(chunk));
stream.on("error", reject);
stream.on("end", () => resolve(hash.digest("hex")));
});
}
async function collectApiDocs() {
const docs = [];
if (!(await exists(apisDir))) {
return docs;
}
const products = await readdir(apisDir, { withFileTypes: true });
for (const productEntry of products) {
if (!productEntry.isDirectory()) continue;
const product = productEntry.name;
const productDir = path.join(apisDir, product);
const files = await readdir(productDir, { withFileTypes: true });
for (const file of files) {
if (!file.isFile() || !file.name.endsWith(".md")) continue;
const fullPath = path.join(productDir, file.name);
const relPath = path.relative(root, fullPath);
const text = await readFile(fullPath, "utf8");
const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() || file.name;
const language = file.name.endsWith(".zh.md") ? "zh" : file.name.endsWith(".en.md") ? "en" : "unknown";
const id = `${product}/${file.name.replace(/\.md$/, "")}`;
const sourceUrls = [...text.matchAll(/https:\/\/[^\s)]+/g)].map((m) =>
m[0].replace(/[",)}>]+$/g, ""),
);
docs.push({
id,
title,
product,
language,
path: relPath,
url: `${repository.raw_base_url}/${relPath}`,
tags: Array.from(new Set([product, ...id.split(/[/.]/).filter(Boolean)])),
source_urls: Array.from(new Set(sourceUrls)),
});
}
}
return docs.sort((a, b) => a.id.localeCompare(b.id));
}
async function buildSkillPackage(skillName, version) {
await mkdir(packagesDir, { recursive: true });
const packageName = `${skillName}-${version}.tar.gz`;
const packagePath = path.join(packagesDir, packageName);
await rm(packagePath, { force: true });
const result = spawnSync("tar", ["-czf", packagePath, "-C", skillsDir, skillName], {
cwd: root,
stdio: "inherit",
});
if (result.status !== 0) {
throw new Error(`tar failed for ${skillName}`);
}
const fileStat = await stat(packagePath);
return {
packageName,
packagePath,
sizeBytes: fileStat.size,
digest: await sha256(packagePath),
};
}
async function collectSkills(apiDocs) {
const skills = [];
if (!(await exists(skillsDir))) {
return skills;
}
const entries = await readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillName = entry.name;
const skillDir = path.join(skillsDir, skillName);
const metadataPath = path.join(skillDir, "skill.registry.json");
const skillPath = path.join(skillDir, "SKILL.md");
if (!(await exists(metadataPath)) || !(await exists(skillPath))) {
continue;
}
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
const pkg = await buildSkillPackage(skillName, metadata.version);
const relatedDocs = (metadata.api_docs || []).map((docId) => {
const doc = apiDocs.find((item) => item.id === docId);
return doc || { id: docId };
});
skills.push({
name: skillName,
version: metadata.version,
title: metadata.title,
description: metadata.description,
path: `skills/${skillName}`,
raw_url: `${repository.raw_base_url}/skills/${skillName}/SKILL.md`,
package: `packages/${pkg.packageName}`,
package_url: `${repository.raw_base_url}/packages/${pkg.packageName}`,
sha256: pkg.digest,
size_bytes: pkg.sizeBytes,
api_docs: relatedDocs,
tags: metadata.tags || [],
agents: metadata.agents || ["codex"],
updated_at: metadata.updated_at,
});
}
return skills.sort((a, b) => a.name.localeCompare(b.name));
}
async function main() {
await mkdir(registryDir, { recursive: true });
const apis = await collectApiDocs();
const skills = await collectSkills(apis);
const manifest = {
schema_version: "1.0.0",
generated_at: new Date().toISOString(),
repository,
skills,
apis,
};
await writeFile(path.join(registryDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`Built registry: ${skills.length} skills, ${apis.length} API docs`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

99
tools/install.sh Executable file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
SKILL_NAME="${1:-}"
VERSION="${2:-latest}"
REPO_URL="${OMNICLAW_SKILLS_REPO:-https://git.omniclaw.store/zsb/omniclaw-skills.git}"
RAW_BASE_URL="${OMNICLAW_SKILLS_RAW_BASE_URL:-https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main}"
DEST_DIR="${CODEX_HOME:-$HOME/.codex}/skills"
if [[ -z "$SKILL_NAME" ]]; then
echo "Usage: install.sh <skill-name> [version]" >&2
exit 2
fi
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing required command: $1" >&2
exit 2
fi
}
require_cmd mktemp
require_cmd node
require_cmd tar
require_cmd shasum
tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT
manifest="$tmp_dir/manifest.json"
if [[ -d "$RAW_BASE_URL" && -f "$RAW_BASE_URL/registry/manifest.json" ]]; then
cp "$RAW_BASE_URL/registry/manifest.json" "$manifest"
elif command -v curl >/dev/null 2>&1; then
curl -fsSL "$RAW_BASE_URL/registry/manifest.json" -o "$manifest" || true
elif command -v wget >/dev/null 2>&1; then
wget -qO "$manifest" "$RAW_BASE_URL/registry/manifest.json" || true
else
: > "$manifest"
fi
if [[ ! -s "$manifest" ]]; then
require_cmd git
git clone --depth 1 "$REPO_URL" "$tmp_dir/repo"
if [[ ! -f "$tmp_dir/repo/registry/manifest.json" ]]; then
echo "Unable to find registry/manifest.json from $REPO_URL" >&2
exit 1
fi
cp "$tmp_dir/repo/registry/manifest.json" "$manifest"
RAW_BASE_URL="$tmp_dir/repo"
fi
node_script='
const fs = require("fs");
const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
const name = process.argv[2];
const version = process.argv[3];
const skill = manifest.skills.find((item) => item.name === name && (version === "latest" || item.version === version));
if (!skill) {
console.error(`Skill not found: ${name}@${version}`);
process.exit(1);
}
console.log(JSON.stringify(skill));
'
skill_json="$(node -e "$node_script" "$manifest" "$SKILL_NAME" "$VERSION")"
package_path="$(printf '%s' "$skill_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>console.log(JSON.parse(s).package));')"
expected_sha="$(printf '%s' "$skill_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>console.log(JSON.parse(s).sha256));')"
archive="$tmp_dir/skill.tar.gz"
if [[ -d "$RAW_BASE_URL" ]]; then
cp "$RAW_BASE_URL/$package_path" "$archive"
elif command -v curl >/dev/null 2>&1; then
curl -fsSL "$RAW_BASE_URL/$package_path" -o "$archive"
else
wget -qO "$archive" "$RAW_BASE_URL/$package_path"
fi
actual_sha="$(shasum -a 256 "$archive" | awk '{print $1}')"
if [[ "$actual_sha" != "$expected_sha" ]]; then
echo "Checksum mismatch for $package_path" >&2
echo "expected: $expected_sha" >&2
echo "actual: $actual_sha" >&2
exit 1
fi
mkdir -p "$DEST_DIR"
target="$DEST_DIR/$SKILL_NAME"
if [[ -e "$target" ]]; then
backup="$target.backup.$(date +%Y%m%d%H%M%S)"
mv "$target" "$backup"
echo "Existing skill moved to $backup"
fi
tar -xzf "$archive" -C "$DEST_DIR"
echo "Installed $SKILL_NAME to $DEST_DIR/$SKILL_NAME"
echo "Restart Codex to pick up new skills."

105
tools/validate.mjs Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
const root = path.resolve(new URL("..", import.meta.url).pathname);
const skillsDir = path.join(root, "skills");
const apisDir = path.join(root, "apis");
const errors = [];
async function exists(file) {
try {
await stat(file);
return true;
} catch {
return false;
}
}
function check(condition, message) {
if (!condition) errors.push(message);
}
function parseSkillFrontmatter(text, file) {
const match = text.match(/^---\n([\s\S]*?)\n---/);
check(Boolean(match), `${file}: missing YAML frontmatter`);
if (!match) return {};
const data = {};
for (const line of match[1].split("\n")) {
const parts = line.split(":");
if (parts.length < 2) continue;
const key = parts.shift().trim();
const value = parts.join(":").trim().replace(/^"|"$/g, "");
data[key] = value;
}
return data;
}
async function validateSkills() {
if (!(await exists(skillsDir))) return;
const entries = await readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const name = entry.name;
const dir = path.join(skillsDir, name);
const skillFile = path.join(dir, "SKILL.md");
const registryFile = path.join(dir, "skill.registry.json");
const agentFile = path.join(dir, "agents", "openai.yaml");
check(await exists(skillFile), `${name}: missing SKILL.md`);
check(await exists(registryFile), `${name}: missing skill.registry.json`);
check(await exists(agentFile), `${name}: missing agents/openai.yaml`);
if (!(await exists(skillFile)) || !(await exists(registryFile))) continue;
const skillText = await readFile(skillFile, "utf8");
const frontmatter = parseSkillFrontmatter(skillText, `${name}/SKILL.md`);
check(frontmatter.name === name, `${name}: frontmatter name must equal folder name`);
check(Boolean(frontmatter.description), `${name}: missing frontmatter description`);
let metadata;
try {
metadata = JSON.parse(await readFile(registryFile, "utf8"));
} catch (error) {
errors.push(`${name}: invalid skill.registry.json: ${error.message}`);
continue;
}
check(metadata.name === name, `${name}: metadata name must equal folder name`);
check(Boolean(metadata.version), `${name}: missing version`);
check(Boolean(metadata.title), `${name}: missing title`);
check(Boolean(metadata.description), `${name}: missing description`);
}
}
async function validateApiDocs() {
if (!(await exists(apisDir))) return;
const products = await readdir(apisDir, { withFileTypes: true });
for (const productEntry of products) {
if (!productEntry.isDirectory()) continue;
const productDir = path.join(apisDir, productEntry.name);
const files = (await readdir(productDir)).filter((name) => name.endsWith(".md"));
const stems = new Map();
for (const file of files) {
const stem = file.replace(/\.(zh|en)\.md$/, "");
if (!stems.has(stem)) stems.set(stem, new Set());
if (file.endsWith(".zh.md")) stems.get(stem).add("zh");
if (file.endsWith(".en.md")) stems.get(stem).add("en");
const text = await readFile(path.join(productDir, file), "utf8");
check(/^#\s+.+/m.test(text), `${productEntry.name}/${file}: missing H1 title`);
}
for (const [stem, languages] of stems) {
check(languages.has("zh"), `${productEntry.name}/${stem}: missing zh doc`);
check(languages.has("en"), `${productEntry.name}/${stem}: missing en doc`);
}
}
}
await validateSkills();
await validateApiDocs();
if (errors.length > 0) {
console.error(errors.map((item) => `- ${item}`).join("\n"));
process.exit(1);
}
console.log("Validation passed.");