Compare commits

..

7 Commits

Author SHA1 Message Date
Jim Liu 宝玉 e1c0ff7c02 chore: release v2.2.1 2026-05-26 00:53:56 -05:00
Jim Liu 宝玉 860dd36bb6 docs(baoyu-image-gen): surface build-batch.ts in Usage and clarify {baseDir} script paths
Add the outline.md + prompts/ → batch.json one-liner to SKILL.md's Usage section so the
build-batch helper is discoverable next to --batchfile, and update build-batch.ts --help
to print the bun / npx-y bun invocations with the {baseDir}/scripts/... layout used by
the rest of the skill.
2026-05-26 00:52:51 -05:00
Jim Liu 宝玉 aab0e28823 chore: release v2.2.0 2026-05-25 00:21:09 -05:00
Jim Liu 宝玉 95cdad2a2a docs(baoyu-article-illustrator,baoyu-comic,baoyu-infographic,baoyu-slide-deck,baoyu-xhs-images): point codex-imagegen flow at baoyu-image-gen --provider codex-cli
Add a `Codex via codex exec` branch to each skill's backend-selection
ladder and ship a per-skill references/codex-imagegen.md with the
invocation contract (preferred baoyu-image-gen --provider codex-cli
path, direct-wrapper fallback, parameter notes, stdout schema, batch
semantics).
2026-05-25 00:18:41 -05:00
Jim Liu 宝玉 8838161729 docs(baoyu-cover-image): redirect codex-imagegen invocation through baoyu-image-gen --provider codex-cli
Replace the long inline `scripts/codex-imagegen.sh` invocation block in
SKILL.md with a pointer to references/codex-imagegen.md. The reference
documents the preferred `baoyu-image-gen --provider codex-cli` path and
keeps the direct-wrapper fallback for runtimes without baoyu-image-gen.
2026-05-25 00:18:32 -05:00
Jim Liu 宝玉 49e9c46ca4 feat(baoyu-image-gen): add codex-cli provider wrapping codex-imagegen
Expose Codex CLI's built-in image_gen tool through baoyu-image-gen's
standard CLI + batch flow as a dedicated provider. The provider spawns
the bundled scripts/codex-imagegen/main.ts (synced from
packages/baoyu-codex-imagegen/src/) so the skill remains self-contained
and inherits retry, cache, file-lock, and JSONL logging. No
OPENAI_API_KEY required — uses the user's Codex subscription.

New env vars: BAOYU_CODEX_IMAGEGEN_{BIN,CACHE_DIR,TIMEOUT_MS,RETRIES,LOG_FILE}.
Hyphenated provider names now resolve under-scored env vars (e.g.,
BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY). codex-cli is never auto-selected
— pin via --provider or default_provider in EXTEND.md.
2026-05-25 00:18:27 -05:00
Jim Liu 宝玉 23fac03691 refactor(codex-imagegen): extract backend to packages/baoyu-codex-imagegen workspace
Move the codex-imagegen wrapper out of scripts/ into its own workspace
package alongside baoyu-md, baoyu-chrome-cdp, and baoyu-fetch. Drop the
bash shim — src/main.ts now carries a `#\!/usr/bin/env bun` shebang and
serves as the sole entrypoint. Add scripts/sync-codex-imagegen.sh so
skills/baoyu-image-gen/scripts/codex-imagegen/ can be regenerated from
packages/baoyu-codex-imagegen/src/ for skill self-containment. Update
CLAUDE.md, docs/codex-imagegen-backend.md, and the CI workflow paths
accordingly.
2026-05-25 00:18:16 -05:00
50 changed files with 1835 additions and 100 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "2.1.0"
"version": "2.2.1"
},
"plugins": [
{
+5 -7
View File
@@ -3,13 +3,11 @@ name: codex-imagegen tests
on:
push:
paths:
- 'scripts/codex-imagegen/**'
- 'scripts/codex-imagegen.sh'
- 'packages/baoyu-codex-imagegen/**'
- '.github/workflows/codex-imagegen-tests.yml'
pull_request:
paths:
- 'scripts/codex-imagegen/**'
- 'scripts/codex-imagegen.sh'
- 'packages/baoyu-codex-imagegen/**'
- '.github/workflows/codex-imagegen-tests.yml'
workflow_dispatch:
@@ -30,11 +28,11 @@ jobs:
run: bun --version
- name: Run unit tests
working-directory: scripts/codex-imagegen
working-directory: packages/baoyu-codex-imagegen
run: bun test
- name: Bundle smoke test (catches import/syntax errors)
run: bun build --target=node scripts/codex-imagegen/main.ts --outfile /tmp/main-build.js
run: bun build --target=node packages/baoyu-codex-imagegen/src/main.ts --outfile /tmp/main-build.js
- name: Help output smoke test
run: bun scripts/codex-imagegen/main.ts --help
run: bun packages/baoyu-codex-imagegen/src/main.ts --help
+18
View File
@@ -2,6 +2,24 @@
English | [中文](./CHANGELOG.zh.md)
## 2.2.1 - 2026-05-26
### Documentation
- `baoyu-image-gen`: surface `scripts/build-batch.ts` in SKILL.md's Usage section so the `outline.md` + `prompts/``batch.json` workflow (e.g., `baoyu-article-illustrator` output) is discoverable next to `--batchfile`. Clarify that all `scripts/...` paths in SKILL.md are relative to `{baseDir}` and point the Generation Mode table at `{baseDir}/scripts/build-batch.ts`. Update `build-batch.ts --help` to print `bun` / `npx -y bun` invocations with the `{baseDir}/scripts/...` layout used by the rest of the skill
## 2.2.0 - 2026-05-25
### Features
- `baoyu-image-gen`: new `codex-cli` provider that wraps the `codex-imagegen` backend so the Codex `image_gen` tool is reachable through the standard `--provider` / batch / EXTEND.md flow. Uses the user's Codex subscription — no `OPENAI_API_KEY` required. Adds env vars `BAOYU_CODEX_IMAGEGEN_{BIN,CACHE_DIR,TIMEOUT_MS,RETRIES,LOG_FILE}` and resolves hyphenated provider names for `BAOYU_IMAGE_GEN_<PROVIDER>_*` overrides (e.g., `BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY`). `codex-cli` is never auto-selected — pin via `--provider codex-cli` or `default_provider: codex-cli` in EXTEND.md
### Refactor
- `codex-imagegen`: extracted from `scripts/codex-imagegen/` + `scripts/codex-imagegen.sh` to its own workspace package at `packages/baoyu-codex-imagegen/`. The bash shim is gone; `src/main.ts` now carries a `#!/usr/bin/env bun` shebang and is the sole entrypoint (`bun packages/baoyu-codex-imagegen/src/main.ts …` or, without bun on `PATH`, `npx -y bun …`). `scripts/sync-codex-imagegen.sh` keeps `skills/baoyu-image-gen/scripts/codex-imagegen/` in sync for skill self-containment
### Documentation
- `baoyu-cover-image`: replace the long inline `scripts/codex-imagegen.sh` invocation block in `SKILL.md` with a pointer to `references/codex-imagegen.md`, documenting the preferred `baoyu-image-gen --provider codex-cli` path and keeping the direct-wrapper fallback
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-infographic`, `baoyu-slide-deck`, `baoyu-xhs-images`: add a `Codex via codex exec` branch to each skill's backend-selection ladder and ship a per-skill `references/codex-imagegen.md` with the invocation contract (preferred path, fallback, stdout schema, batch semantics)
- `docs/codex-imagegen-backend.md` and `CLAUDE.md`: update path layout and invocation examples to reflect the new workspace package location
## 2.1.0 - 2026-05-24
### Features
+18
View File
@@ -2,6 +2,24 @@
[English](./CHANGELOG.md) | 中文
## 2.2.1 - 2026-05-26
### 文档
- `baoyu-image-gen`:在 SKILL.md 的 Usage 段落中显式展示 `scripts/build-batch.ts`,使 `outline.md` + `prompts/``batch.json` 流程(例如 `baoyu-article-illustrator` 的产物)能与 `--batchfile` 并列被发现。明确 SKILL.md 中所有 `scripts/...` 路径均相对 `{baseDir}`,并把 Generation Mode 表中的指引改为 `{baseDir}/scripts/build-batch.ts``build-batch.ts --help` 同步打印 `bun` / `npx -y bun` 调用,与 skill 其它脚本统一使用 `{baseDir}/scripts/...` 写法
## 2.2.0 - 2026-05-25
### 新功能
- `baoyu-image-gen`:新增 `codex-cli` provider,封装 `codex-imagegen` 后端,让 Codex 内置的 `image_gen` 工具也能走标准的 `--provider` / 批量 / EXTEND.md 流程。使用用户自己的 Codex 订阅,无需 `OPENAI_API_KEY`。新增环境变量 `BAOYU_CODEX_IMAGEGEN_{BIN,CACHE_DIR,TIMEOUT_MS,RETRIES,LOG_FILE}``BAOYU_IMAGE_GEN_<PROVIDER>_*` 类覆盖项支持带连字符的 provider 名称(如 `BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY`)。`codex-cli` **不会**自动选用,需通过 `--provider codex-cli` 或 EXTEND.md 的 `default_provider: codex-cli` 显式指定
### 重构
- `codex-imagegen`:从 `scripts/codex-imagegen/` + `scripts/codex-imagegen.sh` 拆分为独立的 workspace 包 `packages/baoyu-codex-imagegen/`。bash 包装器已移除,`src/main.ts` 自带 `#!/usr/bin/env bun` shebang,作为唯一入口(`bun packages/baoyu-codex-imagegen/src/main.ts …`,若 `PATH` 中无 bun 则 `npx -y bun …`)。新增 `scripts/sync-codex-imagegen.sh` 同步 `skills/baoyu-image-gen/scripts/codex-imagegen/`,保持 skill 自包含
### 文档
- `baoyu-cover-image``SKILL.md` 中较长的 `scripts/codex-imagegen.sh` 内联调用块替换为指向 `references/codex-imagegen.md` 的链接,文档明确优先走 `baoyu-image-gen --provider codex-cli`、并保留直接调用 wrapper 的回退路径
- `baoyu-article-illustrator``baoyu-comic``baoyu-infographic``baoyu-slide-deck``baoyu-xhs-images`:在各自 backend 选择阶梯中新增 `Codex via codex exec` 分支,并附带 per-skill 的 `references/codex-imagegen.md`(优先路径、回退、stdout schema、批量语义)
- `docs/codex-imagegen-backend.md``CLAUDE.md`:同步新的 workspace 路径与调用示例
## 2.1.0 - 2026-05-24
### 新功能
+6 -4
View File
@@ -68,19 +68,21 @@ Skills that render images MUST declare the backend-selection convention **inline
### `codex-imagegen` Backend
A backend for non-Codex runtimes (e.g., Claude Code) that generates images by spawning `codex exec --json --sandbox danger-full-access` and delegating to Codex CLI's built-in `image_gen` tool. Uses the user's Codex subscription — no `OPENAI_API_KEY` required.
A backend for non-Codex runtimes (e.g., Claude Code) that generates images by spawning `codex exec --json --sandbox danger-full-access` and delegating to Codex CLI's built-in `image_gen` tool. Uses the user's Codex subscription — no `OPENAI_API_KEY` required. Lives under [packages/baoyu-codex-imagegen](packages/baoyu-codex-imagegen) so it follows the same workspace layout as other shared packages.
Invoke via:
Invoke via (TS entrypoint with `#!/usr/bin/env bun` shebang):
```bash
./scripts/codex-imagegen.sh \
bun packages/baoyu-codex-imagegen/src/main.ts \
--image <output.png> \
--prompt-file prompts/01-cover.md \
--aspect 16:9 \
--cache-dir ~/.cache/baoyu-codex-imagegen
```
Stdout emits a single JSON line: `{"status":"ok","path":...,"bytes":N,...}`. On failure, `{"status":"error","error_kind":...}`. Skills route here by setting `preferred_image_backend: codex-imagegen` in EXTEND.md. Full reference: [docs/codex-imagegen-backend.md](docs/codex-imagegen-backend.md).
Without bun installed: `npx -y bun packages/baoyu-codex-imagegen/src/main.ts …`.
Stdout emits a single JSON line: `{"status":"ok","path":...,"bytes":N,...}`. On failure, `{"status":"error","error_kind":...}`. Skills route here by setting `preferred_image_backend: codex-imagegen` in EXTEND.md, or by running `baoyu-image-gen --provider codex-cli` (which spawns the same wrapper internally). Full reference: [docs/codex-imagegen-backend.md](docs/codex-imagegen-backend.md).
## Release Process
+40 -24
View File
@@ -35,13 +35,13 @@ codex login # signs in with your OpenAI account (subscription)
codex --version # confirm >= 0.130
```
`bun` is preferred for running the wrapper. On macOS:
`bun` is required for running the wrapper. On macOS:
```bash
brew install oven-sh/bun/bun
```
If `bun` is missing, the shell entrypoint falls back to `npx -y bun`.
If `bun` is not on `PATH`, fall back to `npx -y bun packages/baoyu-codex-imagegen/src/main.ts …`.
## Usage
@@ -49,20 +49,32 @@ If `bun` is missing, the shell entrypoint falls back to `npx -y bun`.
```bash
# Inline prompt
./scripts/codex-imagegen.sh \
bun packages/baoyu-codex-imagegen/src/main.ts \
--image /tmp/cat.png \
--prompt "A friendly orange cat, watercolor"
# Prompt from file
./scripts/codex-imagegen.sh \
bun packages/baoyu-codex-imagegen/src/main.ts \
--image cover.png \
--prompt-file prompts/01-cover.md \
--aspect 16:9
# Verbose mode for debugging
./scripts/codex-imagegen.sh -v --image dog.png --prompt "A corgi" --aspect 1:1
bun packages/baoyu-codex-imagegen/src/main.ts -v --image dog.png --prompt "A corgi" --aspect 1:1
```
### Through `baoyu-image-gen`
```bash
${BUN_X} skills/baoyu-image-gen/scripts/main.ts \
--provider codex-cli \
--prompt "A friendly orange cat, watercolor" \
--image /tmp/cat.png \
--ar 1:1
```
The `codex-cli` provider spawns the bundled `codex-imagegen` TS entrypoint internally and surfaces its retry/cache machinery through baoyu-image-gen's standard CLI + batch flow.
On success, stdout emits a single JSON line:
```json
@@ -80,7 +92,7 @@ Image-generating skills (e.g., `baoyu-cover-image`, `baoyu-article-illustrator`)
preferred_image_backend: codex-imagegen
```
When the LLM runs the skill, it reads the preference and — guided by the `### codex-imagegen Backend` section in `CLAUDE.md` — invokes `scripts/codex-imagegen.sh`.
When the LLM runs the skill, it reads the preference and — guided by the `### codex-imagegen Backend` section in `CLAUDE.md` — invokes `bun packages/baoyu-codex-imagegen/src/main.ts`.
> **Note**: The integration is mediated by the LLM reading `CLAUDE.md`. It is not a hard binding. If a skill does not route to the backend automatically, mentioning it explicitly in the prompt works.
@@ -191,24 +203,26 @@ On failure, exit code is `1` and the JSON contains `error` and `error_kind`:
## Architecture
```
scripts/codex-imagegen.sh # thin bash entrypoint
scripts/codex-imagegen/
├── main.ts # parseArgs → cache → lock → retry loop → emit JSON
├── types.ts # CliOptions, GenerateResult, GenError, ErrorKind
├── spawn.ts # spawn codex exec --json --sandbox danger-full-access
├── parser.ts # parse JSONL event stream → toolCalls, usage, thread_id
├── validator.ts # verify image_gen invocation + PNG magic + file size
├── cache.ts # cacheKey(sha256), FileLock, lookup/store
├── logger.ts # JsonLogger (verbose stderr + JSONL file)
├── parser.test.ts
├── cache.test.ts
└── validator.test.ts
packages/baoyu-codex-imagegen/
├── src/
├── main.ts # parseArgs → cache → lock → retry loop → emit JSON (`#!/usr/bin/env bun`)
├── types.ts # CliOptions, GenerateResult, GenError, ErrorKind
├── spawn.ts # spawn codex exec --json --sandbox danger-full-access
├── parser.ts # parse JSONL event stream → toolCalls, usage, thread_id
├── validator.ts # verify image_gen invocation + PNG magic + file size
├── cache.ts # cacheKey(sha256), FileLock, lookup/store
├── logger.ts # JsonLogger (verbose stderr + JSONL file)
├── parser.test.ts
├── cache.test.ts
└── validator.test.ts
├── package.json # workspace package: `bin` → `src/main.ts`, no build step
└── README.md
```
Run tests:
```bash
cd scripts/codex-imagegen && bun test
cd packages/baoyu-codex-imagegen && bun test
```
## Internal Flow
@@ -216,7 +230,7 @@ cd scripts/codex-imagegen && bun test
```mermaid
flowchart LR
CC[Claude Code / any caller]
WRAPPER[scripts/codex-imagegen.sh]
WRAPPER[bun packages/baoyu-codex-imagegen/<br/>src/main.ts]
CODEX["codex exec --json<br/>--sandbox danger-full-access"]
AGENT[Codex agent]
TOOL[image_gen built-in tool]
@@ -239,18 +253,20 @@ flowchart LR
## Design Decisions
1. **Bash entrypoint + TypeScript implementation** — the shell wrapper picks the runtime (`bun` preferred, falling back to `npx -y bun`); TypeScript handles the orchestration, parsing, retry, cache, and logging. This mirrors the project's existing `scripts/*.mjs` and `skills/<skill>/scripts/main.ts` pattern.
1. **Pure TypeScript entrypoint**`src/main.ts` carries a `#!/usr/bin/env bun` shebang and is the sole entry. There is no shell shim: callers either invoke `bun src/main.ts …` directly, run the file as an executable (when `bun` is on `PATH`), or fall back to `npx -y bun src/main.ts …`. This matches the project's `skills/<skill>/scripts/main.ts` convention.
2. **`--sandbox danger-full-access`** — necessary so the spawned agent can `cp`/`mv` the rendered PNG out of `$CODEX_HOME/generated_images/` to the user-specified path. Standard sandboxes block this.
3. **Parse the JSONL event stream** — the final `agent_message` and intermediate `command_execution` events let the wrapper verify what actually happened (was `image_gen` called? did `cp` reach the right destination?), which is far more reliable than scraping freeform stdout.
4. **Infrastructure, not a skill** — this backend is a CLI utility that skills route to via `preferred_image_backend`. It belongs in `scripts/`, not `skills/`, because it has no `SKILL.md` and is never loaded directly by an agent.
4. **Shared package, not a skill** — this backend is a CLI utility that skills route to via `preferred_image_backend` and that `baoyu-image-gen --provider codex-cli` spawns internally. It lives under `packages/` alongside the other shared workspaces (`baoyu-md`, `baoyu-chrome-cdp`, `baoyu-fetch`) because it has no `SKILL.md` and is never loaded directly by an agent.
5. **File lock instead of internal queue** — keeps the implementation small and works across multiple shell sessions or processes invoking the same wrapper concurrently.
## Related Files
| File | Role |
|------|------|
| `scripts/codex-imagegen.sh` | CLI entrypoint |
| `scripts/codex-imagegen/` | TypeScript implementation |
| `packages/baoyu-codex-imagegen/src/main.ts` | TypeScript CLI entrypoint (`#!/usr/bin/env bun`) |
| `packages/baoyu-codex-imagegen/src/` | TypeScript implementation |
| `packages/baoyu-codex-imagegen/package.json` | Workspace manifest |
| `skills/baoyu-image-gen/scripts/providers/codex-cli.ts` | Provider adapter that lets `baoyu-image-gen --provider codex-cli` spawn this wrapper |
| `docs/codex-imagegen-backend.md` | This document |
| `CLAUDE.md` | Tells LLMs how to invoke this backend |
| `.github/workflows/codex-imagegen-tests.yml` | CI unit tests |
+102
View File
@@ -0,0 +1,102 @@
# baoyu-codex-imagegen
Generate images via Codex CLI's built-in `image_gen` tool from non-Codex runtimes (e.g., Claude Code). The wrapper spawns `codex exec --json` and lets the user's existing Codex subscription drive image generation — **no `OPENAI_API_KEY` required**.
This package implements the `preferred_image_backend: codex-imagegen` config key referenced across the `baoyu-skills` plugin and is the engine behind `baoyu-image-gen --provider codex-cli`.
## Layout
```
packages/baoyu-codex-imagegen/
├── src/
│ ├── main.ts # CLI orchestrator (executable via `#!/usr/bin/env bun`)
│ ├── spawn.ts # codex exec child-process wrapper
│ ├── parser.ts # JSONL event-stream parser
│ ├── validator.ts # Output PNG / image_gen-invocation verification
│ ├── cache.ts # SHA256 idempotency cache + file lock
│ ├── logger.ts # Structured JSONL logging
│ ├── types.ts # Shared types and `GenError`
│ └── *.test.ts # Bun unit tests
└── package.json # `bin` points to `src/main.ts`
```
## Prerequisites
```bash
npm install -g @openai/codex
codex login # signs in with your OpenAI account (subscription)
codex --version # confirm >= 0.130
```
`bun` is required for running the wrapper:
```bash
brew install oven-sh/bun/bun
```
If `bun` is not on `PATH`, `npx -y bun src/main.ts …` works as a fallback.
## Usage
```bash
# Inline prompt (executes via shebang once bun is on PATH)
./src/main.ts \
--image /tmp/cat.png \
--prompt "A friendly orange cat, watercolor"
# Or invoke bun explicitly
bun src/main.ts \
--image cover.png \
--prompt-file prompts/01-cover.md \
--aspect 16:9 \
--cache-dir ~/.cache/baoyu-codex-imagegen
# Without bun installed
npx -y bun src/main.ts --image cover.png --prompt "..."
```
Stdout emits a single JSON line:
```json
{"status":"ok","path":"…","bytes":1234567,"elapsed_seconds":62,"thread_id":"…","attempts":1,"cached":false,"usage":{}}
```
On failure:
```json
{"status":"error","path":"…","bytes":0,"error":"…","error_kind":"timeout"}
```
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
## Options
| Flag | Description |
|---|---|
| `--image <path>` | Output PNG path (required) |
| `--prompt <text>` | Prompt text |
| `--prompt-file <path>` | Read prompt from file (mutually exclusive with `--prompt`) |
| `--aspect <ratio>` | Aspect ratio (`1:1`, `16:9`, `9:16`, `4:3`, `2.35:1`). Default: `1:1` |
| `--ref <file>` | Reference image (repeatable) |
| `--timeout <ms>` | Codex exec timeout in ms. Default: `300000` |
| `--retries <n>` | Retry attempts on retryable errors. Default: `2` |
| `--retry-delay <ms>` | Base retry delay (exponential). Default: `1500` |
| `--cache-dir <path>` | Enable idempotency cache. Disabled by default. |
| `--log-file <path>` | Append structured JSONL log |
| `-v, --verbose` | Verbose stderr logging |
| `-h, --help` | Show help |
## Test
```bash
cd packages/baoyu-codex-imagegen
bun test
```
## Trade-offs
- 510× slower than direct OpenAI API calls (except on cache hits)
- Uses your Codex subscription — programmatic use of `codex exec` falls into the same terms as interactive use
- Requires `codex` CLI and active login session
See [`docs/codex-imagegen-backend.md`](../../docs/codex-imagegen-backend.md) for the full background.
@@ -0,0 +1,39 @@
{
"name": "baoyu-codex-imagegen",
"version": "0.1.0",
"type": "module",
"description": "Generate images via Codex CLI's built-in image_gen tool from non-Codex runtimes (Claude Code, Hermes, …).",
"bin": {
"codex-imagegen": "./src/main.ts"
},
"files": [
"src/**/*.ts",
"!src/**/*.test.ts"
],
"exports": {
".": {
"types": "./src/main.ts",
"default": "./src/main.ts"
},
"./src/*": "./src/*"
},
"scripts": {
"test": "bun test",
"smoke": "bun src/main.ts --help"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JimLiu/baoyu-skills.git",
"directory": "packages/baoyu-codex-imagegen"
},
"bugs": {
"url": "https://github.com/JimLiu/baoyu-skills/issues"
},
"homepage": "https://github.com/JimLiu/baoyu-skills/tree/main/packages/baoyu-codex-imagegen#readme",
"publishConfig": {
"access": "public"
},
"engines": {
"bun": ">=1.2.0"
}
}
@@ -1,3 +1,4 @@
#!/usr/bin/env bun
import { readFile, mkdir, copyFile, stat } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
# codex-imagegen: generate images via Codex CLI's built-in image_gen tool
# Thin shell wrapper — implementation in codex-imagegen/main.ts (Bun TypeScript)
#
# Usage: ./codex-imagegen.sh --help
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if command -v bun &>/dev/null; then
BUN_X="bun"
elif command -v npx &>/dev/null; then
BUN_X="npx -y bun"
else
echo "Error: bun or npx required. Install: brew install oven-sh/bun/bun" >&2
exit 1
fi
exec $BUN_X "$SCRIPT_DIR/codex-imagegen/main.ts" "$@"
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# Sync packages/baoyu-codex-imagegen/src/*.ts (excluding tests) into
# skills/baoyu-image-gen/scripts/codex-imagegen/ so the skill stays
# self-contained (no `../../../../packages/...` lookups at runtime).
#
# Run this whenever packages/baoyu-codex-imagegen/src/ changes,
# and always before tagging a release.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC_DIR="$REPO_ROOT/packages/baoyu-codex-imagegen/src"
DST_DIR="$REPO_ROOT/skills/baoyu-image-gen/scripts/codex-imagegen"
if [[ ! -d "$SRC_DIR" ]]; then
echo "Error: source dir missing: $SRC_DIR" >&2
exit 1
fi
mkdir -p "$DST_DIR"
changed=0
for f in cache.ts logger.ts main.ts parser.ts spawn.ts types.ts validator.ts; do
src="$SRC_DIR/$f"
dst="$DST_DIR/$f"
if [[ ! -f "$src" ]]; then
echo "Error: missing source file: $src" >&2
exit 1
fi
if [[ ! -f "$dst" ]] || ! cmp -s "$src" "$dst"; then
cp "$src" "$dst"
echo "synced: $f"
changed=$((changed + 1))
fi
done
chmod +x "$DST_DIR/main.ts"
# Drop any stale .ts files in DST that don't exist in SRC.
for dst in "$DST_DIR"/*.ts; do
[[ -e "$dst" ]] || continue
name="$(basename "$dst")"
case "$name" in
*.test.ts) rm -f "$dst"; echo "removed test artifact: $name" ;;
*)
if [[ ! -f "$SRC_DIR/$name" ]]; then
rm -f "$dst"
echo "removed stale: $name"
changed=$((changed + 1))
fi
;;
esac
done
if [[ "$changed" -eq 0 ]]; then
echo "codex-imagegen sync: up to date"
else
echo "codex-imagegen sync: $changed file(s) updated"
fi
+3 -1
View File
@@ -1,7 +1,7 @@
---
name: baoyu-article-illustrator
description: Analyzes article structure, identifies positions requiring visual aids, generates illustrations with Type × Style × Palette three-dimension approach. Use when user asks to "illustrate article", "add images", "generate images for article", or "为文章配图".
version: 1.117.3
version: 1.117.4
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-article-illustrator
@@ -29,6 +29,7 @@ When this skill needs to render an image, resolve the backend in this order:
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-image-gen`) unless the user has explicitly pinned a different `preferred_image_backend`.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime exposes no native `imagegen` skill but the `codex` CLI is on `PATH` with an active `codex login`, route through `baoyu-image-gen --provider codex-cli` (preferred), or — if baoyu-image-gen is unavailable — invoke the bundled wrapper directly. Details, parameters, and the runtime-discovery procedure live in [references/codex-imagegen.md](references/codex-imagegen.md) — load that file only when this branch is selected.
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-image-gen`), use it.
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
@@ -185,6 +186,7 @@ Full template: [references/workflow.md](references/workflow.md#step-4-generate-o
4. LABELS **MUST** include article-specific data: actual numbers, terms, metrics, quotes
5. **DO NOT** pass ad-hoc inline prompts to `--prompt` without saving prompt files first
6. Select the backend via the `## Image Generation Tools` rule at the top: use whatever is available; if multiple, ask the user once. Do this once per session before any generation.
- **`codex-imagegen` invocation**: when the rule resolves to `codex-imagegen`, see [references/codex-imagegen.md](references/codex-imagegen.md) for the invocation contract (preferred `baoyu-image-gen --provider codex-cli` path, runtime wrapper discovery, parameter notes, stdout schema, batch semantics).
7. **Execution strategy**: Generate in batches per the `## Batch Generation Policy`: backend native batch first, runtime parallel tool calls second, sequential only as fallback. Default batch size is 4 unless EXTEND.md or the current request overrides it.
8. Process references (`direct`/`style`/`palette`) per prompt frontmatter
9. Apply watermark if EXTEND.md enabled
@@ -0,0 +1,65 @@
# `codex-imagegen` Wrapper Invocation
Load this reference only when the [Image Generation Tools](../SKILL.md#image-generation-tools) rule has resolved to `codex-imagegen` — i.e., the current runtime exposes no native `imagegen` skill but `codex` CLI is on `PATH` with an active `codex login`.
## Preferred path: route through `baoyu-image-gen`
If the `baoyu-image-gen` skill is available in this runtime, **always** invoke through it rather than calling the wrapper directly. It handles retry/cache/batch/EXTEND.md preferences uniformly with every other provider.
```bash
${BUN_X} <baoyu-image-gen-base>/scripts/main.ts \
--provider codex-cli \
--image <ABSOLUTE_output> \
--promptfiles <ABSOLUTE_prompts/NN-{type}-{slug}.md> \
--ar <ratio> \
[--ref <ABSOLUTE_file>]...
```
Resolve `<baoyu-image-gen-base>` the same way you resolve any sibling skill — through your runtime's skill registry (`Skill` tool, plugin marketplace, or `$HOME/.baoyu-skills/baoyu-image-gen/`).
## Fallback: spawn the wrapper directly
Only when `baoyu-image-gen` is NOT installed in the current runtime. Discover the wrapper's location at runtime — do NOT hard-code `../../packages/...` from this skill:
1. **Honor explicit override**: if `$BAOYU_CODEX_IMAGEGEN_BIN` is set and points to a real file, use that path. It may be `.ts` (spawn `bun <path>`) or `.sh`/binary (spawn directly).
2. **Search the plugin root**: walk up from this skill's directory looking for `packages/baoyu-codex-imagegen/src/main.ts`. If found, that is the wrapper. Spawn it with `bun`.
3. **Last resort**: tell the user that `codex-imagegen` is not available in this runtime and ask whether to install the `baoyu-skills` plugin (or set `BAOYU_CODEX_IMAGEGEN_BIN`) or pick another backend.
Once located, the invocation shape is:
```bash
bun <WRAPPER>/main.ts \
--image <ABSOLUTE_output> \
--prompt-file <ABSOLUTE_prompts/NN-{type}-{slug}.md> \
--aspect <ratio> \
[--ref <ABSOLUTE_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <ABSOLUTE_jsonl_log_path>]
```
If `bun` is missing, `npx -y bun <WRAPPER>/main.ts ...` works as a fallback.
## Parameter notes
- **All filesystem inputs** are auto-resolved against `process.cwd()` when relative, but agents should pass absolute paths to be robust against cwd drift.
- **`--timeout`** defaults to `300000` (5 min) per `codex exec` attempt. Raise (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
- **`--cache-dir`** is off by default. Enable for repeatable runs to skip redundant generations of the same prompt+aspect+refs.
- **Authentication**: the wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` is read or sent.
## Stdout contract
Single JSON line:
- Success: `{"status":"ok","path":"...","bytes":N,"elapsed_seconds":N,"thread_id":"...","attempts":N,"cached":bool,...}`
- Failure: `{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}`
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
On retryable errors (timeout, spawn_failed, no_image_gen_tool_use, output_missing, invalid_png, agent_refused), ask the user whether to retry or fall back to another backend.
## Batch semantics
- Codex `image_gen` returns **one image per call** (`n=1` only). Multi-image jobs must dispatch one wrapper call per image.
- The wrapper does NOT accept a `--sessionId` flag. Chain/scene consistency must come from `--ref` reference images.
- `--size` and `--quality` are silently ignored — Codex picks pixel dimensions from `--aspect`.
+4 -1
View File
@@ -1,7 +1,7 @@
---
name: baoyu-comic
description: Knowledge comic creator supporting multiple art styles and tones. Creates original educational comics with detailed panel layouts and batch-capable image generation. Use when user asks to create "知识漫画", "教育漫画", "biography comic", "tutorial comic", or "Logicomix-style comic".
version: 1.117.3
version: 1.117.4
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-comic
@@ -33,6 +33,7 @@ When this skill needs to render an image, resolve the backend in this order:
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-image-gen`) unless the user has explicitly pinned a different `preferred_image_backend`.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime exposes no native `imagegen` skill but the `codex` CLI is on `PATH` with an active `codex login`, route through `baoyu-image-gen --provider codex-cli` (preferred), or — if baoyu-image-gen is unavailable — invoke the bundled wrapper directly. Details, parameters, and the runtime-discovery procedure live in [references/codex-imagegen.md](references/codex-imagegen.md) — load that file only when this branch is selected.
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-image-gen`), use it.
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
@@ -247,6 +248,8 @@ Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard →
**Pick a backend once per session** using the `## Image Generation Tools` rule at the top. If the backend is a repo skill (e.g., `baoyu-image-gen`), read its `SKILL.md` and use its documented interface rather than its scripts.
**`codex-imagegen` invocation**: when the rule resolves to `codex-imagegen`, see [references/codex-imagegen.md](references/codex-imagegen.md) for the invocation contract (preferred `baoyu-image-gen --provider codex-cli` path, runtime wrapper discovery, parameter notes, stdout schema, batch semantics — n=1 per call so page batches must dispatch one wrapper call per page).
**7.1 Character sheet** — generate it (to `characters/characters.png`, aspect `4:3`) when the comic is multi-page with recurring characters. Skip for simple presets (e.g., four-panel minimalist) or single-page comics. Compress to JPEG before use-as-`--ref` (`sips -s format jpeg -s formatOptions 80 …` on macOS, `pngquant --quality=65-80 …` elsewhere) to avoid payload failures. The prompt file at `characters/characters.md` must exist before invoking the backend.
**7.2 Pages** — each page's prompt MUST already be at `prompts/NN-{cover|page}-[slug].md` before invoking the backend; the file is the reproducibility record. Strategy depends on the character sheet:
@@ -0,0 +1,65 @@
# `codex-imagegen` Wrapper Invocation
Load this reference only when the [Image Generation Tools](../SKILL.md#image-generation-tools) rule has resolved to `codex-imagegen` — i.e., the current runtime exposes no native `imagegen` skill but `codex` CLI is on `PATH` with an active `codex login`.
## Preferred path: route through `baoyu-image-gen`
If the `baoyu-image-gen` skill is available in this runtime, **always** invoke through it rather than calling the wrapper directly. It handles retry/cache/batch/EXTEND.md preferences uniformly with every other provider.
```bash
${BUN_X} <baoyu-image-gen-base>/scripts/main.ts \
--provider codex-cli \
--image <ABSOLUTE_output> \
--promptfiles <ABSOLUTE_prompts/NN-{cover|page}-[slug].md> \
--ar <ratio> \
[--ref <ABSOLUTE_file>]...
```
Resolve `<baoyu-image-gen-base>` the same way you resolve any sibling skill — through your runtime's skill registry (`Skill` tool, plugin marketplace, or `$HOME/.baoyu-skills/baoyu-image-gen/`).
## Fallback: spawn the wrapper directly
Only when `baoyu-image-gen` is NOT installed in the current runtime. Discover the wrapper's location at runtime — do NOT hard-code `../../packages/...` from this skill:
1. **Honor explicit override**: if `$BAOYU_CODEX_IMAGEGEN_BIN` is set and points to a real file, use that path. It may be `.ts` (spawn `bun <path>`) or `.sh`/binary (spawn directly).
2. **Search the plugin root**: walk up from this skill's directory looking for `packages/baoyu-codex-imagegen/src/main.ts`. If found, that is the wrapper. Spawn it with `bun`.
3. **Last resort**: tell the user that `codex-imagegen` is not available in this runtime and ask whether to install the `baoyu-skills` plugin (or set `BAOYU_CODEX_IMAGEGEN_BIN`) or pick another backend.
Once located, the invocation shape is:
```bash
bun <WRAPPER>/main.ts \
--image <ABSOLUTE_output> \
--prompt-file <ABSOLUTE_prompts/NN-{cover|page}-[slug].md> \
--aspect <ratio> \
[--ref <ABSOLUTE_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <ABSOLUTE_jsonl_log_path>]
```
If `bun` is missing, `npx -y bun <WRAPPER>/main.ts ...` works as a fallback.
## Parameter notes
- **All filesystem inputs** are auto-resolved against `process.cwd()` when relative, but agents should pass absolute paths to be robust against cwd drift.
- **`--timeout`** defaults to `300000` (5 min) per `codex exec` attempt. Raise (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
- **`--cache-dir`** is off by default. Enable for repeatable runs to skip redundant generations of the same prompt+aspect+refs.
- **Authentication**: the wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` is read or sent.
## Stdout contract
Single JSON line:
- Success: `{"status":"ok","path":"...","bytes":N,"elapsed_seconds":N,"thread_id":"...","attempts":N,"cached":bool,...}`
- Failure: `{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}`
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
On retryable errors (timeout, spawn_failed, no_image_gen_tool_use, output_missing, invalid_png, agent_refused), ask the user whether to retry or fall back to another backend.
## Batch semantics
- Codex `image_gen` returns **one image per call** (`n=1` only). Multi-image jobs must dispatch one wrapper call per image.
- The wrapper does NOT accept a `--sessionId` flag. Chain/scene consistency must come from `--ref` reference images.
- `--size` and `--quality` are silently ignored — Codex picks pixel dimensions from `--aspect`.
+3 -15
View File
@@ -1,7 +1,7 @@
---
name: baoyu-cover-image
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 11 color palettes and 7 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
version: 1.117.4
version: 1.117.5
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-cover-image
@@ -29,19 +29,7 @@ When this skill needs to render an image, resolve the backend in this order:
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-image-gen`) unless the user has explicitly pinned a different `preferred_image_backend`.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime does NOT expose a native `imagegen` skill but the `codex` CLI is on `PATH` and `codex login` is active (e.g., Claude Code with Codex CLI installed), invoke the `codex-imagegen` wrapper. **Path resolution**: `scripts/codex-imagegen.sh` lives at the plugin/repo root, NOT relative to your shell cwd. From this `SKILL.md`'s base directory, the wrapper is at `../../scripts/codex-imagegen.sh` — resolve to an absolute path before invoking. Command shape:
```bash
<ABSOLUTE_PLUGIN_ROOT>/scripts/codex-imagegen.sh \
--image <absolute_output_path> \
--prompt-file <absolute_path_to_prompts/NN-cover-[slug].md> \
--aspect <ratio> \
[--ref <absolute_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <absolute_jsonl_log_path>]
```
`--timeout` defaults to 300000 (5 min) per codex exec attempt; raise it (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
All input paths to the wrapper are auto-resolved against the wrapper's `process.cwd()` if you pass relative ones, but agents should pass absolute paths to be robust against cwd drift. Parse the single-line JSON on stdout. On `{"status":"ok",...}` proceed to Step 5. On `{"status":"error","error_kind":...}` report the `error_kind` to the user and (if retryable) ask whether to retry or fall back to another backend. The wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` needed.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime exposes no native `imagegen` skill but the `codex` CLI is on `PATH` with an active `codex login`, route through `baoyu-image-gen --provider codex-cli` (preferred), or — if baoyu-image-gen is unavailable — invoke the bundled wrapper directly. Details, parameters, and the runtime-discovery procedure live in [references/codex-imagegen.md](references/codex-imagegen.md) — load that file only when this branch is selected.
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-image-gen`), use it.
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
@@ -228,7 +216,7 @@ Save to `prompts/cover.md`. Template: [references/workflow/prompt-template.md](r
- `direct` usage → pass via `--ref` (use ref-capable backend)
- `style`/`palette` → extract traits, append to prompt
5. **Generate**: Call the chosen backend with the prompt file, output path, aspect ratio.
- **`codex-imagegen`**: invoke `<ABSOLUTE_PLUGIN_ROOT>/scripts/codex-imagegen.sh` (NOT a cwd-relative `./scripts/...` — resolve the absolute path from this skill's base directory: `../../scripts/codex-imagegen.sh`) with `--image <ABSOLUTE_output>` `--prompt-file <ABSOLUTE_prompts/01-cover-[slug].md>` `--aspect <ratio>` (add `--ref <ABSOLUTE_file>` per reference, `--cache-dir ~/.cache/baoyu-codex-imagegen` to enable the idempotency cache, `--timeout <ms>` to override the default 300000 / 5-min per-attempt limit on slow networks). All input paths to the wrapper are auto-resolved against its `process.cwd()` if relative, but passing absolutes is more robust. Read the stdout JSON; act on `status` and `error_kind`.
- **`codex-imagegen`**: see [references/codex-imagegen.md](references/codex-imagegen.md) for the invocation contract (preferred `baoyu-image-gen --provider codex-cli` path, runtime wrapper discovery, parameter notes, stdout schema, batch semantics).
- **Codex `imagegen` (native)** or other runtime-native tools / `baoyu-image-gen` skill: per the rule in `## Image Generation Tools` above.
6. On failure: auto-retry once
@@ -0,0 +1,65 @@
# `codex-imagegen` Wrapper Invocation
Load this reference only when the [Image Generation Tools](../SKILL.md#image-generation-tools) rule has resolved to `codex-imagegen` — i.e., the current runtime exposes no native `imagegen` skill but `codex` CLI is on `PATH` with an active `codex login`.
## Preferred path: route through `baoyu-image-gen`
If the `baoyu-image-gen` skill is available in this runtime, **always** invoke through it rather than calling the wrapper directly. It handles retry/cache/batch/EXTEND.md preferences uniformly with every other provider.
```bash
${BUN_X} <baoyu-image-gen-base>/scripts/main.ts \
--provider codex-cli \
--image <ABSOLUTE_output> \
--promptfiles <ABSOLUTE_prompts/01-cover-[slug].md> \
--ar <ratio> \
[--ref <ABSOLUTE_file>]...
```
Resolve `<baoyu-image-gen-base>` the same way you resolve any sibling skill — through your runtime's skill registry (`Skill` tool, plugin marketplace, or `$HOME/.baoyu-skills/baoyu-image-gen/`).
## Fallback: spawn the wrapper directly
Only when `baoyu-image-gen` is NOT installed in the current runtime. Discover the wrapper's location at runtime — do NOT hard-code `../../packages/...` from this skill:
1. **Honor explicit override**: if `$BAOYU_CODEX_IMAGEGEN_BIN` is set and points to a real file, use that path. It may be `.ts` (spawn `bun <path>`) or `.sh`/binary (spawn directly).
2. **Search the plugin root**: walk up from this skill's directory looking for `packages/baoyu-codex-imagegen/src/main.ts`. If found, that is the wrapper. Spawn it with `bun`.
3. **Last resort**: tell the user that `codex-imagegen` is not available in this runtime and ask whether to install the `baoyu-skills` plugin (or set `BAOYU_CODEX_IMAGEGEN_BIN`) or pick another backend.
Once located, the invocation shape is:
```bash
bun <WRAPPER>/main.ts \
--image <ABSOLUTE_output> \
--prompt-file <ABSOLUTE_prompts/01-cover-[slug].md> \
--aspect <ratio> \
[--ref <ABSOLUTE_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <ABSOLUTE_jsonl_log_path>]
```
If `bun` is missing, `npx -y bun <WRAPPER>/main.ts ...` works as a fallback.
## Parameter notes
- **All filesystem inputs** are auto-resolved against `process.cwd()` when relative, but agents should pass absolute paths to be robust against cwd drift.
- **`--timeout`** defaults to `300000` (5 min) per `codex exec` attempt. Raise (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
- **`--cache-dir`** is off by default. Enable for repeatable runs to skip redundant generations of the same prompt+aspect+refs.
- **Authentication**: the wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` is read or sent.
## Stdout contract
Single JSON line:
- Success: `{"status":"ok","path":"...","bytes":N,"elapsed_seconds":N,"thread_id":"...","attempts":N,"cached":bool,...}`
- Failure: `{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}`
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
On retryable errors (timeout, spawn_failed, no_image_gen_tool_use, output_missing, invalid_png, agent_refused), ask the user whether to retry or fall back to another backend.
## Batch semantics
- Codex `image_gen` returns **one image per call** (`n=1` only). Multi-image jobs must dispatch one wrapper call per image.
- The wrapper does NOT accept a `--sessionId` flag. Chain/scene consistency must come from `--ref` reference images.
- `--size` and `--quality` are silently ignored — Codex picks pixel dimensions from `--aspect`.
+22 -8
View File
@@ -1,7 +1,7 @@
---
name: baoyu-image-gen
description: AI image generation with OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope, Z.AI GLM-Image, MiniMax, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
version: 2.0.0
version: 2.1.0
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-image-gen
@@ -27,7 +27,7 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
## Script Directory
`{baseDir}` = this SKILL.md's directory. Main script: `{baseDir}/scripts/main.ts`. Resolve `${BUN_X}`: prefer `bun`; else `npx -y bun`; else suggest `brew install oven-sh/bun/bun`.
`{baseDir}` = this SKILL.md's directory. All `scripts/...` paths below are relative to `{baseDir}`. Main script: `{baseDir}/scripts/main.ts`. Batch payload helper: `{baseDir}/scripts/build-batch.ts`. Resolve `${BUN_X}`: prefer `bun`; else `npx -y bun`; else suggest `brew install oven-sh/bun/bun`.
## Step 0: Load Preferences ⛔ BLOCKING
@@ -81,8 +81,15 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider d
# OpenAI GPT Image 2
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider openai --model gpt-image-2
# Codex CLI (uses logged-in Codex subscription — no OPENAI_API_KEY required; requires `codex` on PATH)
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider codex-cli --ar 16:9
# Batch mode
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4
# Build a batch file from outline.md + prompts/ (e.g. baoyu-article-illustrator output)
${BUN_X} {baseDir}/scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4
```
## Reference-Image Identity Preservation
@@ -104,7 +111,7 @@ When the user wants a person/object preserved from reference images:
| `--image <path>` | Output image path (required in single-image mode) |
| `--batchfile <path>` | JSON batch file for multi-image generation |
| `--jobs <count>` | Worker count for batch mode (default: auto, max from config, built-in default 10) |
| `--provider google\|openai\|azure\|openrouter\|dashscope\|zai\|minimax\|jimeng\|seedream\|replicate` | Force provider (default: auto-detect) |
| `--provider google\|openai\|azure\|openrouter\|dashscope\|zai\|minimax\|jimeng\|seedream\|replicate\|codex-cli` | Force provider (default: auto-detect; `codex-cli` is never auto-selected — must be pinned via CLI or EXTEND.md) |
| `--model <id>`, `-m` | Model ID — see provider references for defaults and allowed values |
| `--ar <ratio>` | Aspect ratio (`16:9`, `1:1`, `4:3`, …) |
| `--size <WxH>` | Explicit size (e.g., `1024x1024`; for `gpt-image-2`, width/height must be multiples of 16, max edge 3840px, ratio no wider than 3:1) |
@@ -137,8 +144,13 @@ When the user wants a person/object preserved from reference images:
| `OPENAI_IMAGE_API_DIALECT` | `openai-native` \| `ratio-metadata` |
| `OPENROUTER_HTTP_REFERER`, `OPENROUTER_TITLE` | Optional OpenRouter attribution |
| `BAOYU_IMAGE_GEN_MAX_WORKERS` | Override batch worker cap |
| `BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY` | Per-provider concurrency (e.g., `BAOYU_IMAGE_GEN_REPLICATE_CONCURRENCY`) |
| `BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY` | Per-provider concurrency (e.g., `BAOYU_IMAGE_GEN_REPLICATE_CONCURRENCY`; for codex-cli use `BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY`) |
| `BAOYU_IMAGE_GEN_<PROVIDER>_START_INTERVAL_MS` | Per-provider start-gap |
| `BAOYU_CODEX_IMAGEGEN_BIN` | Override the codex-imagegen wrapper path for the `codex-cli` provider (default: bundled `scripts/codex-imagegen/main.ts`; accepts `.ts` or legacy `.sh`/binary) |
| `BAOYU_CODEX_IMAGEGEN_CACHE_DIR` | Enable idempotency cache for the `codex-cli` provider (off by default) |
| `BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS` | Per-attempt `codex exec` timeout for the `codex-cli` provider (default: 300000 ms) |
| `BAOYU_CODEX_IMAGEGEN_RETRIES` | Wrapper-side retry attempts on retryable errors for the `codex-cli` provider (default: 2) |
| `BAOYU_CODEX_IMAGEGEN_LOG_FILE` | Append JSONL diagnostic log for the `codex-cli` provider |
**Load priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
@@ -149,10 +161,10 @@ When the user wants a person/object preserved from reference images:
If the user wants to use their Codex subscription / GPT Image 2 entitlement without an OpenAI API key, route through a Codex-native backend instead of this skill's `openai` provider:
- In Codex runtime: use the native `imagegen` skill/tool.
- In non-Codex runtimes with `codex` CLI installed and logged in: use the repo-level `scripts/codex-imagegen.sh` wrapper when the calling skill supports it (for example `baoyu-cover-image`). Resolve it from the plugin/repo root and pass absolute prompt/output/reference paths.
- In non-Codex runtimes with `codex` CLI installed and logged in: use `baoyu-image-gen --provider codex-cli` (preferred — it gives you the same retry / cache / batch flow as every other provider). The provider spawns the bundled `scripts/codex-imagegen/main.ts`; the same code lives upstream at `packages/baoyu-codex-imagegen/src/main.ts` for standalone callers.
- In Hermes runtimes with a native `image_generate` tool: use that tool as a fallback, and state whether reference images were passed directly or reconstructed from extracted traits.
Do not modify the existing `openai` provider to silently consume Codex OAuth. If first-class Codex OAuth support is added to `baoyu-image-gen`, implement it as a distinct provider (for example `openai-codex`) with its own auth, route, request shape, docs, and tests. See `references/codex-oauth-vs-openai-api-key.md`.
Do not modify the existing `openai` provider to silently consume Codex OAuth. The first-class Codex-CLI path is the dedicated `codex-cli` provider, which has its own auth (Codex login), route (`codex exec`), request shape, and tests. See `references/codex-oauth-vs-openai-api-key.md`.
## Model Resolution
@@ -194,13 +206,15 @@ Each provider has its own quirks (model families, size rules, ref support, limit
| MiniMax (image-01, subject-reference) | `references/providers/minimax.md` |
| OpenRouter (multimodal models, `/chat/completions` flow) | `references/providers/openrouter.md` |
| Replicate (nano-banana, Seedream, Wan) | `references/providers/replicate.md` |
| Codex CLI (wraps bundled `scripts/codex-imagegen/`; Codex login, no `OPENAI_API_KEY`) | `references/providers/codex-cli.md` |
## Provider Selection
1. `--ref` provided + no `--provider` → auto-select Google → OpenAI → Azure → OpenRouter → Replicate → Seedream → MiniMax (MiniMax's subject reference is more specialized toward character/portrait consistency)
2. `--provider` specified → use it (if `--ref`, must be google/openai/azure/openrouter/replicate/seedream/minimax)
2. `--provider` specified → use it (if `--ref`, must be google/openai/azure/openrouter/replicate/seedream/minimax/codex-cli)
3. Only one API key present → use that provider
4. Multiple keys → default priority: Google → OpenAI → Azure → OpenRouter → DashScope → Z.AI → MiniMax → Replicate → Jimeng → Seedream
5. `codex-cli` is **never auto-selected** — set `default_provider: codex-cli` in EXTEND.md or pass `--provider codex-cli`. It spawns `codex exec` via the bundled `scripts/codex-imagegen/main.ts` TS entrypoint (run with `bun`) and uses the user's Codex subscription (no `OPENAI_API_KEY`). Requires `codex` on `PATH` with an active `codex login`.
## Quality Presets
@@ -232,7 +246,7 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`.
| One image, or 1-2 simple images | Sequential | Lower coordination overhead, easier debugging |
| Multiple images with saved prompt files | Batch (`--batchfile`) | Reuses finalized prompts, applies shared throttling/retries, predictable throughput |
| Each image still needs its own reasoning / prompt writing / style exploration | Subagents | Work is still exploratory, each needs independent analysis |
| Input is `outline.md` + `prompts/` (e.g. from `baoyu-article-illustrator`) | Batch — use `scripts/build-batch.ts` to assemble the payload | The outline + prompt files already contain everything needed |
| Input is `outline.md` + `prompts/` (e.g. from `baoyu-article-illustrator`) | Batch — use `{baseDir}/scripts/build-batch.ts` to assemble the payload | The outline + prompt files already contain everything needed |
Rule of thumb: once prompt files are saved and the task is "generate all of these", prefer batch over subagents. Use subagents only when generation is coupled with per-image thinking or divergent creative exploration.
@@ -19,7 +19,7 @@ This is expected. The `openai` provider uses the public OpenAI Images API and ne
2. If it fails only because `OPENAI_API_KEY` is missing, do not leave the user waiting.
3. Prefer a Codex/native raster backend in this order:
- Codex runtime native `imagegen` skill/tool, if available.
- Repo-level `scripts/codex-imagegen.sh`, if `codex` CLI is installed/logged in and the calling skill supports the wrapper.
- `baoyu-image-gen --provider codex-cli` (preferred — wraps the bundled `scripts/codex-imagegen/main.ts`; the underlying repo-level package lives at `packages/baoyu-codex-imagegen/src/main.ts` for standalone callers), if `codex` CLI is installed/logged in.
- Hermes native `image_generate`, if available.
4. Be transparent about reference-image behavior:
- If the fallback backend accepts references, pass the reference images.
@@ -7,9 +7,9 @@ Codex / ChatGPT login is different. Codex image generation is driven by Codex OA
## What to use instead
- If running inside Codex and the native `imagegen` skill/tool is available, use it directly.
- If running outside Codex but the `codex` CLI is installed and logged in, use the repo-level `scripts/codex-imagegen.sh` wrapper when the calling skill supports it. The wrapper invokes `codex exec` and the Codex `image_gen` tool; no `OPENAI_API_KEY` is required.
- If running outside Codex but the `codex` CLI is installed and logged in, call `baoyu-image-gen --provider codex-cli` (preferred). It spawns the bundled `scripts/codex-imagegen/main.ts` and surfaces its retry/cache/log machinery through baoyu-image-gen's standard CLI + batch flow. Standalone callers outside this skill can run the same code at `packages/baoyu-codex-imagegen/src/main.ts`. Both invoke `codex exec` and the Codex `image_gen` tool; no `OPENAI_API_KEY` is required.
- If running inside Hermes and a native `image_generate` tool is available, use that as a runtime-native fallback. Be explicit about whether reference images are passed directly or only reconstructed from extracted traits.
- If the user wants `baoyu-image-gen` itself to support Codex OAuth, add a distinct provider such as `openai-codex` rather than modifying the existing `openai` provider.
- `baoyu-image-gen` already exposes a distinct `codex-cli` provider (wraps the bundled `scripts/codex-imagegen/`); do not modify the existing `openai` provider to add Codex OAuth.
## Reference-image prompting note
@@ -11,7 +11,7 @@ description: EXTEND.md YAML schema for baoyu-image-gen user preferences
---
version: 1
default_provider: null # google|openai|azure|openrouter|dashscope|zai|minimax|replicate|null (null = auto-detect)
default_provider: null # google|openai|azure|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|codex-cli|null (null = auto-detect; codex-cli is never auto-detected — pin it here or via --provider)
default_quality: null # normal|2k|null (null = use default: 2k)
@@ -30,6 +30,7 @@ default_model:
zai: null # e.g., "glm-image"
minimax: null # e.g., "image-01"
replicate: null # e.g., "google/nano-banana-2"
codex-cli: null # Logical label only — Codex image_gen has no user-selectable model. Default: "codex-image-gen"
batch:
max_workers: 10
@@ -58,6 +59,9 @@ batch:
minimax:
concurrency: 3
start_interval_ms: 1100
codex-cli:
concurrency: 1
start_interval_ms: 2000
---
```
@@ -79,6 +83,7 @@ batch:
| `default_model.zai` | string\|null | null | Z.AI default model |
| `default_model.minimax` | string\|null | null | MiniMax default model |
| `default_model.replicate` | string\|null | null | Replicate default model |
| `default_model.codex-cli` | string\|null | null | Codex-CLI logical label (Codex image_gen has no user-selectable model) |
| `batch.max_workers` | int\|null | 10 | Batch worker cap |
| `batch.provider_limits.<provider>.concurrency` | int\|null | provider default | Max simultaneous requests per provider |
| `batch.provider_limits.<provider>.start_interval_ms` | int\|null | provider default | Minimum gap between request starts per provider |
@@ -0,0 +1,81 @@
# Codex CLI (`--provider codex-cli`)
Read when the user picks `--provider codex-cli`, sets `default_provider: codex-cli`, or asks for "Codex image generation without an OpenAI API key". This provider is a thin baoyu-image-gen wrapper around the bundled `scripts/codex-imagegen/main.ts` (synced from `packages/baoyu-codex-imagegen`), which spawns `codex exec --json --sandbox danger-full-access` and routes the request to Codex CLI's built-in `image_gen` tool. The Codex CLI uses the **user's Codex / ChatGPT subscription** — no `OPENAI_API_KEY` is read or sent.
## Prerequisites
```bash
npm install -g @openai/codex
codex login # signs in with the user's OpenAI / Codex account
codex --version # confirm >= 0.130
```
`bun` is required for running the underlying wrapper (`scripts/codex-imagegen/main.ts`, carrying `#!/usr/bin/env bun`). If `bun` is missing from the runtime, `npx -y bun` works as a fallback.
## Selection
- **Never auto-selected.** `detectProvider` only picks `codex-cli` when it is pinned explicitly: pass `--provider codex-cli` or set `default_provider: codex-cli` in EXTEND.md.
- Choose this provider when:
- The user has a Codex subscription and explicitly does **not** want to manage an OpenAI API key.
- You need Codex's specific `image_gen` behavior or quality.
- Avoid this provider when latency matters — Codex CLI is typically 510× slower than direct OpenAI / Google API calls (except on cache hits).
## Supported flags
| Flag | Behavior |
|------|----------|
| `--prompt <text>` / `--promptfiles <files>` | Required. Written to a temp file and passed to the wrapper as `--prompt-file`. |
| `--image <path>` | Required. Final output PNG location. |
| `--ar <ratio>` | Mapped to wrapper's `--aspect`. Supported by Codex: `1:1` (default), `16:9`, `9:16`, `4:3`, `2.35:1`. |
| `--ref <files...>` | Mapped to wrapper's repeated `--ref`. Codex's `image_gen` accepts reference images for style/composition guidance. |
| `--n` | Must be `1`. `validateArgs` throws if `n > 1` because Codex `image_gen` returns a single image per call. |
| `--imageApiDialect` | Not applicable. Throws if set to a non-default value. |
| `--size`, `--imageSize`, `--quality` | Silently ignored — Codex picks pixel dimensions from the aspect ratio. |
| `--model`, `-m` | Logical label only. The wrapper does not forward a model selector to Codex; the underlying engine is whichever model Codex's `image_gen` currently uses. Default label: `codex-image-gen`. |
## Environment variables
| Variable | Effect |
|----------|--------|
| `BAOYU_CODEX_IMAGEGEN_BIN` | Override the wrapper path. Default: bundled `scripts/codex-imagegen/main.ts` resolved relative to this skill's installed location. Accepts a `.ts` file (spawned with `bun`) or a legacy `.sh`/binary (spawned directly). |
| `BAOYU_CODEX_IMAGEGEN_CACHE_DIR` | Enable the wrapper's idempotency cache. Disabled by default; set to e.g. `~/.cache/baoyu-codex-imagegen` for high-value reuse. |
| `BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS` | Per-attempt `codex exec` timeout in ms. Default: `300000` (5 min). Raise for slow networks or large prompts. |
| `BAOYU_CODEX_IMAGEGEN_RETRIES` | Wrapper-side retry attempts on retryable errors. Default: `2` (3 total attempts). |
| `BAOYU_CODEX_IMAGEGEN_LOG_FILE` | Append a structured JSONL diagnostic log. Useful when triaging timeouts or `agent_refused` errors. |
| `BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY` | Batch-mode concurrency for the `codex-cli` provider. Default: `1` — Codex exec is a heavy single-process workflow; raising this rarely helps. |
| `BAOYU_IMAGE_GEN_CODEX_CLI_START_INTERVAL_MS` | Batch-mode minimum start-gap. Default: `2000` ms. |
## Error model
The wrapper emits a single JSON line on stdout. On failure:
```json
{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}
```
The provider re-throws each wrapper error as `Invalid codex-cli result (<error_kind>): <message>`. The `"Invalid "` prefix triggers `isRetryableGenerationError` to mark it **non-retryable** in baoyu-image-gen's outer retry loop — the wrapper has already retried internally per `BAOYU_CODEX_IMAGEGEN_RETRIES`, so re-spawning Codex from main.ts would only multiply latency without changing the outcome.
`error_kind` values to expect:
| Kind | Cause | Action |
|------|-------|--------|
| `codex_not_installed` | `codex` not on `PATH` or unreadable | `npm install -g @openai/codex`, then `codex login`. |
| `invalid_args` | Programmer error in the spawn invocation | Inspect provider source; usually a path-injection guard fired. |
| `prompt_file_missing` | Temp prompt file vanished mid-call | Retry once; check `$TMPDIR` permissions. |
| `spawn_failed` | OS / process-launch failure | Verify `bun` or `npx` is installed; check filesystem permissions. |
| `timeout` | `codex exec` exceeded `--timeout` | Raise `BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS`; check network. |
| `no_image_gen_tool_use` | Codex agent answered without calling `image_gen` | Often transient — retry. If persistent, refine the prompt. |
| `output_missing` / `invalid_png` | Agent reported success but file is absent or not a valid PNG | Retry; check disk space. |
| `agent_refused` | Codex agent refused (policy or content) | Adjust the prompt; surface the refusal to the user. |
| `lock_busy` | Another `codex-imagegen` invocation holds the file lock | Wait or set a distinct `--cache-dir` per concurrent caller. |
## Trade-offs
- Slow: 510× direct OpenAI API latency (except cache hits).
- Subject to the same TOS as interactive `codex exec` use — programmatic invocation from baoyu-image-gen is the same usage class.
- Stateful: requires `codex login` to be live; an expired session manifests as `codex_not_installed` or `agent_refused`.
## See also
- `references/codex-oauth-vs-openai-api-key.md` — why Codex OAuth is not interchangeable with `OPENAI_API_KEY`.
- `references/codex-image2-fallback.md` — when to fall back to `codex-cli` from a failed `openai` provider call.
@@ -77,8 +77,19 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic portrait" --image out.p
# Replicate Wan 2.7 Image Pro
${BUN_X} {baseDir}/scripts/main.ts --prompt "A concept frame" --image out.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
# Codex CLI (uses Codex / ChatGPT subscription — no OPENAI_API_KEY; requires `codex` on PATH and `codex login`)
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic portrait" --image out.png --provider codex-cli --ar 16:9
# Codex CLI with reference images (style/composition guidance)
${BUN_X} {baseDir}/scripts/main.ts --prompt "Match this color palette" --image out.png --provider codex-cli --ref source.png --ar 1:1
```
Notes on `codex-cli`:
- Never auto-selected — pin via `--provider codex-cli` or `default_provider: codex-cli` in EXTEND.md.
- Only `n=1` supported (Codex `image_gen` returns one image per call); `--size`, `--imageSize`, `--quality`, and `--imageApiDialect` are ignored or rejected.
- Typically 510× slower than direct OpenAI / Google API calls (except on cache hits). Tune via `BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS`, `BAOYU_CODEX_IMAGEGEN_RETRIES`, and `BAOYU_CODEX_IMAGEGEN_CACHE_DIR`.
## Batch Mode
```bash
@@ -28,7 +28,8 @@ type PromptReference = {
function printUsage(): void {
console.log(`Usage:
npx -y tsx scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments
bun <baseDir>/scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments
npx -y tsx <baseDir>/scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments
Options:
--outline <path> Path to outline.md
@@ -0,0 +1,80 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile, copyFile, stat } from "node:fs/promises";
import { existsSync, openSync, closeSync } from "node:fs";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
export function cacheKey(prompt: string, aspect: string, refs: string[]): string {
const h = createHash("sha256");
h.update(prompt);
h.update("|");
h.update(aspect);
h.update("|");
for (const r of [...refs].sort()) h.update(r);
return h.digest("hex").slice(0, 16);
}
export async function lookupCache(cacheDir: string, key: string): Promise<string | null> {
const entry = path.join(cacheDir, `${key}.png`);
try {
const s = await stat(entry);
if (s.size > 1000) return entry;
} catch {}
return null;
}
export async function storeCache(cacheDir: string, key: string, sourcePath: string): Promise<void> {
await mkdir(cacheDir, { recursive: true });
const entry = path.join(cacheDir, `${key}.png`);
await copyFile(sourcePath, entry);
}
export class FileLock {
private fd: number | null = null;
constructor(private lockPath: string) {}
async acquire(timeoutMs = 30_000): Promise<void> {
const start = Date.now();
await mkdir(path.dirname(this.lockPath), { recursive: true });
while (Date.now() - start < timeoutMs) {
try {
this.fd = openSync(this.lockPath, "wx");
return;
} catch (e: any) {
if (e.code !== "EEXIST") throw e;
if (await this.isStale()) {
try {
await this.release(true);
} catch {}
continue;
}
await delay(200);
}
}
throw new Error(`Failed to acquire lock at ${this.lockPath} within ${timeoutMs}ms`);
}
private async isStale(): Promise<boolean> {
try {
const s = await stat(this.lockPath);
return Date.now() - s.mtimeMs > 10 * 60 * 1000;
} catch {
return true;
}
}
async release(force = false): Promise<void> {
if (this.fd != null) {
try {
closeSync(this.fd);
} catch {}
this.fd = null;
}
if (existsSync(this.lockPath) || force) {
const { unlink } = await import("node:fs/promises");
try {
await unlink(this.lockPath);
} catch {}
}
}
}
@@ -0,0 +1,39 @@
import { appendFile, mkdir } from "node:fs/promises";
import path from "node:path";
export interface LogEntry {
ts: string;
level: "info" | "warn" | "error";
event: string;
[k: string]: unknown;
}
export class JsonLogger {
constructor(private logFile: string | null, public verbose: boolean) {}
async log(level: LogEntry["level"], event: string, extra: Record<string, unknown> = {}): Promise<void> {
const entry: LogEntry = { ts: new Date().toISOString(), level, event, ...extra };
const line = JSON.stringify(entry);
if (this.verbose) process.stderr.write(`[${level}] ${event} ${jsonExtras(extra)}\n`);
if (this.logFile) {
await mkdir(path.dirname(this.logFile), { recursive: true });
await appendFile(this.logFile, line + "\n", "utf-8");
}
}
info(event: string, extra?: Record<string, unknown>) {
return this.log("info", event, extra);
}
warn(event: string, extra?: Record<string, unknown>) {
return this.log("warn", event, extra);
}
error(event: string, extra?: Record<string, unknown>) {
return this.log("error", event, extra);
}
}
function jsonExtras(extra: Record<string, unknown>): string {
const entries = Object.entries(extra);
if (entries.length === 0) return "";
return entries.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
}
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env bun
import { readFile, mkdir, copyFile, stat } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { GenError, type CliOptions, type GenerateResult } from "./types.ts";
import { runCodexExec } from "./spawn.ts";
import { findCpToTarget, verifyImageGenWasInvoked, verifyOutput } from "./validator.ts";
import { cacheKey, lookupCache, storeCache, FileLock } from "./cache.ts";
import { JsonLogger } from "./logger.ts";
const HELP = `codex-imagegen — generate images via Codex CLI's image_gen tool
Usage:
codex-imagegen --image <output.png> [--prompt <text> | --prompt-file <path>] [options]
Required:
--image <path> Output PNG path
--prompt <text> Prompt text (or use --prompt-file)
--prompt-file <path> Read prompt from file
Options:
--aspect <ratio> Aspect ratio (1:1, 16:9, 9:16, 4:3, 2.35:1). Default: 1:1
--ref <file> Reference image (repeatable)
--timeout <ms> Codex exec timeout in ms. Default: 300000
--retries <n> Retry attempts on retryable errors. Default: 2
--retry-delay <ms> Base retry delay (exponential). Default: 1500
--cache-dir <path> Enable idempotency cache. Disabled by default.
--log-file <path> Append JSONL log
-v, --verbose Verbose stderr logging
-h, --help Show this help
Stdout: single JSON line on success or failure.
`;
const SHELL_METACHAR = /[;|&`$<>\n\r()'"]/;
function assertSafePath(label: string, value: string): void {
if (SHELL_METACHAR.test(value)) {
throw new GenError(
"invalid_args",
`${label} contains shell metacharacters and would be unsafe to interpolate into the codex instruction: ${value}`,
false,
);
}
}
function parseArgs(argv: string[]): CliOptions {
const opts: CliOptions = {
prompt: "",
promptFile: null,
outputPath: "",
aspect: "1:1",
refImages: [],
timeoutMs: 300_000,
retries: 2,
retryDelayMs: 1500,
cacheDir: null,
logFile: null,
verbose: false,
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const next = () => argv[++i];
switch (a) {
case "--prompt": opts.prompt = next(); break;
case "--prompt-file": opts.promptFile = next(); break;
case "--image": opts.outputPath = next(); break;
case "--aspect": opts.aspect = next(); break;
case "--ref": opts.refImages.push(next()); break;
case "--timeout": opts.timeoutMs = Number(next()); break;
case "--retries": opts.retries = Number(next()); break;
case "--retry-delay": opts.retryDelayMs = Number(next()); break;
case "--cache-dir": opts.cacheDir = next(); break;
case "--log-file": opts.logFile = next(); break;
case "-v":
case "--verbose": opts.verbose = true; break;
case "-h":
case "--help": process.stdout.write(HELP); process.exit(0);
default: throw new GenError("invalid_args", `Unknown argument: ${a}`, false);
}
}
if (!opts.outputPath) throw new GenError("invalid_args", "--image is required", false);
if (opts.prompt && opts.promptFile) {
throw new GenError("invalid_args", "--prompt and --prompt-file are mutually exclusive", false);
}
if (!opts.prompt && !opts.promptFile) {
throw new GenError("invalid_args", "--prompt or --prompt-file required", false);
}
// Resolve every filesystem path to absolute up front, so behavior is
// independent of the caller's cwd. This matters when the wrapper is
// invoked from a skill running in an arbitrary working directory.
const cwd = process.cwd();
const toAbs = (p: string) => (path.isAbsolute(p) ? p : path.resolve(cwd, p));
opts.outputPath = toAbs(opts.outputPath);
if (opts.promptFile) opts.promptFile = toAbs(opts.promptFile);
opts.refImages = opts.refImages.map(toAbs);
if (opts.cacheDir) opts.cacheDir = toAbs(opts.cacheDir);
if (opts.logFile) opts.logFile = toAbs(opts.logFile);
// The output and ref paths are interpolated raw into the agent instruction
// sent to `codex exec --sandbox danger-full-access`. A path containing shell
// metacharacters could be misread by the agent's shell when it cp's the
// result into place. Reject upfront rather than trusting the agent to quote.
assertSafePath("--image path", opts.outputPath);
for (const ref of opts.refImages) assertSafePath("--ref path", ref);
return opts;
}
async function loadPrompt(opts: CliOptions): Promise<string> {
if (opts.prompt) return opts.prompt;
const file = opts.promptFile!;
try {
return await readFile(file, "utf-8");
} catch {
throw new GenError("prompt_file_missing", `Prompt file not found: ${file}`, false);
}
}
function buildInstruction(prompt: string, opts: CliOptions): string {
const refHint = opts.refImages.length > 0
? `\nREFERENCE IMAGES (attached above): ${opts.refImages.length} image(s) provided for style/composition guidance.\n`
: "";
return `You have an internal tool called image_gen for image generation. Use it.
TASK: Generate an image with the spec below, then save to disk.
PROMPT:
${prompt}
ASPECT RATIO: ${opts.aspect}
OUTPUT PATH: ${opts.outputPath}
${refHint}
STEPS:
1. Call image_gen with the prompt and aspect ratio above${opts.refImages.length > 0 ? " (using the attached reference images for guidance)" : ""}.
2. Move or copy the resulting image from Codex default location ($CODEX_HOME/generated_images/...) to: ${opts.outputPath}
3. Verify with: ls -la ${opts.outputPath}
4. Reply with ONLY this JSON line (no markdown fences, no other text):
{"status":"ok","path":"${opts.outputPath}","bytes":<file_size_in_bytes>}
HARD CONSTRAINTS:
- Do NOT use curl, wget, Python, or any external API.
- Do NOT use bash to fabricate an image; only image_gen produces real pixels.
- Use ONLY the image_gen internal tool.`;
}
async function attemptGenerate(
opts: CliOptions,
instruction: string,
attempt: number,
log: JsonLogger,
): Promise<{ bytes: number; threadId: string | null; usage: any; toolCalls: any[] }> {
await log.info("attempt.start", { attempt, output: opts.outputPath, aspect: opts.aspect });
const run = await runCodexExec({
instruction,
timeoutMs: opts.timeoutMs,
refImages: opts.refImages,
});
await log.info("codex.completed", {
duration_ms: run.durationMs,
thread_id: run.threadId,
tool_calls: run.toolCalls.length,
usage: run.usage,
raw_log: run.rawLogPath,
});
// verify: thread id must be present
if (!run.threadId) {
throw new GenError("agent_refused", "No thread id in event stream");
}
// verify: image_gen was actually invoked (check $CODEX_HOME/generated_images/{threadId}/)
const ver = await verifyImageGenWasInvoked(run.threadId);
if (!ver.ok) {
// secondary verify: did tool_calls include cp/mv from generated_images to our target
if (!findCpToTarget(run.toolCalls, opts.outputPath)) {
throw new GenError("no_image_gen_tool_use", `image_gen was not invoked: ${ver.reason}`);
}
}
// verify output
const { bytes } = await verifyOutput(opts.outputPath);
return {
bytes,
threadId: run.threadId,
usage: run.usage,
toolCalls: run.toolCalls.map((tc) => ({ tool: tc.tool, status: tc.status })),
};
}
async function generate(opts: CliOptions, log: JsonLogger): Promise<GenerateResult> {
const startEpoch = Date.now();
const prompt = await loadPrompt(opts);
// Cache lookup
if (opts.cacheDir) {
const key = cacheKey(prompt, opts.aspect, opts.refImages);
const cached = await lookupCache(opts.cacheDir, key);
if (cached) {
await mkdir(path.dirname(opts.outputPath), { recursive: true });
await copyFile(cached, opts.outputPath);
const s = await stat(opts.outputPath);
await log.info("cache.hit", { key, source: cached });
return {
status: "ok",
path: opts.outputPath,
bytes: s.size,
elapsed_seconds: 0,
thread_id: null,
attempts: 0,
cached: true,
usage: null,
tool_calls: [],
};
}
await log.info("cache.miss", { key });
}
// lock to prevent concurrent codex exec
const lockDir = opts.cacheDir ?? path.join(homedir(), ".cache", "baoyu-codex-imagegen");
const lock = new FileLock(path.join(lockDir, "codex-exec.lock"));
try {
await lock.acquire(60_000);
} catch (e) {
throw new GenError("lock_busy", String(e), false);
}
await mkdir(path.dirname(opts.outputPath), { recursive: true });
const instruction = buildInstruction(prompt, opts);
let lastErr: GenError | null = null;
let lastAttempt = 0;
try {
for (let attempt = 1; attempt <= opts.retries + 1; attempt++) {
lastAttempt = attempt;
try {
const result = await attemptGenerate(opts, instruction, attempt, log);
// write to cache
if (opts.cacheDir) {
const key = cacheKey(prompt, opts.aspect, opts.refImages);
await storeCache(opts.cacheDir, key, opts.outputPath);
await log.info("cache.stored", { key });
}
return {
status: "ok",
path: opts.outputPath,
bytes: result.bytes,
elapsed_seconds: Math.round((Date.now() - startEpoch) / 1000),
thread_id: result.threadId,
attempts: attempt,
cached: false,
usage: result.usage,
tool_calls: result.toolCalls,
};
} catch (e) {
lastErr = e instanceof GenError ? e : new GenError("spawn_failed", String(e));
await log.warn("attempt.failed", {
attempt,
kind: lastErr.kind,
retryable: lastErr.retryable,
error: lastErr.message,
});
if (!lastErr.retryable || attempt > opts.retries) break;
const wait = opts.retryDelayMs * Math.pow(2, attempt - 1);
await log.info("retry.wait", { wait_ms: wait, next_attempt: attempt + 1 });
await delay(wait);
}
}
} finally {
await lock.release();
}
const err = lastErr ?? new GenError("spawn_failed", "Unknown failure");
err.attempts = lastAttempt;
throw err;
}
async function main() {
let opts: CliOptions;
try {
opts = parseArgs(process.argv.slice(2));
} catch (e) {
const err = e instanceof GenError ? e : new GenError("invalid_args", String(e), false);
process.stderr.write(`Error: ${err.message}\n`);
process.exit(2);
}
const log = new JsonLogger(opts.logFile, opts.verbose);
await log.info("start", { output: opts.outputPath, aspect: opts.aspect, refs: opts.refImages.length });
try {
const result = await generate(opts, log);
await log.info("done", { bytes: result.bytes, attempts: result.attempts, cached: result.cached });
process.stdout.write(JSON.stringify(result) + "\n");
process.exit(0);
} catch (e) {
const err = e instanceof GenError ? e : new GenError("spawn_failed", String(e));
await log.error("failed", { kind: err.kind, error: err.message, attempts: err.attempts ?? 0 });
const out: GenerateResult = {
status: "error",
path: opts.outputPath,
bytes: 0,
elapsed_seconds: 0,
thread_id: null,
attempts: err.attempts ?? 0,
cached: false,
usage: null,
tool_calls: [],
error: err.message,
error_kind: err.kind,
};
process.stdout.write(JSON.stringify(out) + "\n");
process.exit(1);
}
}
main();
@@ -0,0 +1,64 @@
import type { CodexRunResult, ToolCall, TokenUsage } from "./types.ts";
export function parseEventStream(raw: string): Omit<CodexRunResult, "rawLogPath" | "durationMs"> {
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
let threadId: string | null = null;
let agentMessage: string | null = null;
let usage: TokenUsage | null = null;
const toolCallsById = new Map<string, ToolCall>();
for (const line of lines) {
let event: any;
try {
event = JSON.parse(line);
} catch {
continue;
}
const type = event?.type;
if (type === "thread.started") {
threadId = event.thread_id ?? null;
} else if (type === "item.started" || type === "item.completed") {
const item = event.item;
if (!item?.id) continue;
const tc: ToolCall = {
id: item.id,
tool: deriveToolName(item),
status: item.status ?? (type === "item.completed" ? "completed" : "in_progress"),
command: item.command,
};
toolCallsById.set(item.id, tc);
if (item.type === "agent_message" && type === "item.completed") {
agentMessage = String(item.text ?? "");
}
} else if (type === "turn.completed") {
const u = event.usage;
if (u) {
usage = {
input: u.input_tokens ?? 0,
cached_input: u.cached_input_tokens ?? 0,
output: u.output_tokens ?? 0,
reasoning: u.reasoning_output_tokens ?? 0,
};
}
}
}
return {
threadId,
toolCalls: Array.from(toolCallsById.values()),
agentMessage,
usage,
};
}
function deriveToolName(item: any): string {
if (item.type === "command_execution") return "shell";
if (item.type === "agent_message") return "agent_message";
if (item.type === "image_gen" || item.type === "image_generation") return "image_gen";
if (typeof item.tool === "string") return item.tool;
return item.type ?? "unknown";
}
export function hasImageGenInvocation(toolCalls: ToolCall[]): boolean {
return toolCalls.some((tc) => tc.tool === "image_gen");
}
@@ -0,0 +1,81 @@
import { spawn } from "node:child_process";
import { writeFile, mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { GenError, type CodexRunResult } from "./types.ts";
import { parseEventStream } from "./parser.ts";
export interface SpawnInput {
instruction: string;
timeoutMs: number;
refImages?: string[];
}
export async function runCodexExec(input: SpawnInput): Promise<CodexRunResult> {
const start = Date.now();
const logDir = await mkdtemp(path.join(tmpdir(), "codex-imggen-"));
const rawLogPath = path.join(logDir, "stream.jsonl");
// --skip-git-repo-check: lets the wrapper run from non-git cwds
// (e.g. /tmp, or a skill installed under ~/.claude/plugins/...).
// Without it, codex refuses with "Not inside a trusted directory".
const args = [
"exec",
"--json",
"--sandbox",
"danger-full-access",
"--skip-git-repo-check",
];
for (const img of input.refImages ?? []) {
args.push("--image", img);
}
args.push("-");
let timedOut = false;
const child = spawn("codex", args, { stdio: ["pipe", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.stdin.write(input.instruction);
child.stdin.end();
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
setTimeout(() => child.kill("SIGKILL"), 2000);
}, input.timeoutMs);
const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {
child.on("close", (code, signal) => resolve({ code, signal }));
});
clearTimeout(timer);
await writeFile(rawLogPath, stdout + (stderr ? `\n--- stderr ---\n${stderr}` : ""));
if (timedOut) {
throw new GenError("timeout", `codex exec exceeded ${input.timeoutMs}ms (log: ${rawLogPath})`);
}
if (exit.code !== 0) {
if (stderr.includes("command not found") || stderr.includes("not found: codex")) {
throw new GenError("codex_not_installed", "codex CLI not installed", false);
}
throw new GenError(
"spawn_failed",
`codex exec exited ${exit.code} signal=${exit.signal} (log: ${rawLogPath})`,
);
}
const parsed = parseEventStream(stdout);
return {
...parsed,
rawLogPath,
durationMs: Date.now() - start,
};
}
@@ -0,0 +1,79 @@
export interface CliOptions {
prompt: string;
promptFile: string | null;
outputPath: string;
aspect: string;
refImages: string[];
timeoutMs: number;
retries: number;
retryDelayMs: number;
cacheDir: string | null;
logFile: string | null;
verbose: boolean;
}
export interface ToolCall {
id: string;
tool: string;
status: string;
command?: string;
}
export interface TokenUsage {
input: number;
cached_input: number;
output: number;
reasoning: number;
}
export interface CodexRunResult {
threadId: string | null;
toolCalls: ToolCall[];
agentMessage: string | null;
usage: TokenUsage | null;
rawLogPath: string;
durationMs: number;
}
export interface GenerateResult {
status: "ok" | "error";
path: string;
bytes: number;
elapsed_seconds: number;
thread_id: string | null;
attempts: number;
cached: boolean;
usage: TokenUsage | null;
tool_calls: { tool: string; status: string }[];
error?: string;
error_kind?: ErrorKind;
}
export type ErrorKind =
| "codex_not_installed"
| "invalid_args"
| "prompt_file_missing"
| "spawn_failed"
| "timeout"
| "no_image_gen_tool_use"
| "output_missing"
| "invalid_png"
| "agent_refused"
| "lock_busy";
export const RETRYABLE: ReadonlySet<ErrorKind> = new Set([
"spawn_failed",
"timeout",
"no_image_gen_tool_use",
"output_missing",
"invalid_png",
"agent_refused",
]);
export class GenError extends Error {
attempts?: number;
constructor(public kind: ErrorKind, message: string, public retryable?: boolean) {
super(message);
this.retryable = retryable ?? RETRYABLE.has(kind);
}
}
@@ -0,0 +1,55 @@
import { stat, readdir } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import { GenError } from "./types.ts";
import type { ToolCall } from "./types.ts";
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
export function codexHome(): string {
return process.env.CODEX_HOME ?? path.join(homedir(), ".codex");
}
export async function verifyImageGenWasInvoked(threadId: string | null): Promise<{ ok: boolean; reason?: string }> {
if (!threadId) return { ok: false, reason: "no thread id" };
const dir = path.join(codexHome(), "generated_images", threadId);
try {
const entries = await readdir(dir);
const pngs = entries.filter((e) => e.toLowerCase().endsWith(".png"));
if (pngs.length === 0) return { ok: false, reason: `no PNG in ${dir}` };
return { ok: true };
} catch (e: any) {
return { ok: false, reason: `cannot read ${dir}: ${e?.code ?? e?.message}` };
}
}
export function findCpToTarget(toolCalls: ToolCall[], target: string): boolean {
return toolCalls.some(
(tc) =>
tc.tool === "shell" &&
typeof tc.command === "string" &&
(tc.command.includes(target) || tc.command.includes(path.basename(target))) &&
/\b(cp|mv|cat)\b/.test(tc.command) &&
tc.command.includes("generated_images"),
);
}
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
let s;
try {
s = await stat(outputPath);
} catch {
throw new GenError("output_missing", `Output file not created: ${outputPath}`);
}
if (s.size < 1000) {
throw new GenError("invalid_png", `Output file too small (${s.size} bytes)`);
}
const file = Bun.file(outputPath);
const head = new Uint8Array(await file.slice(0, 8).arrayBuffer());
for (let i = 0; i < PNG_MAGIC.length; i++) {
if (head[i] !== PNG_MAGIC[i]) {
throw new GenError("invalid_png", `Output is not a valid PNG (magic mismatch)`);
}
}
return { bytes: s.size };
}
+24 -10
View File
@@ -64,6 +64,7 @@ const DEFAULT_PROVIDER_RATE_LIMITS: Record<Provider, ProviderRateLimit> = {
jimeng: { concurrency: 3, startIntervalMs: 1100 },
seedream: { concurrency: 3, startIntervalMs: 1100 },
azure: { concurrency: 3, startIntervalMs: 1100 },
"codex-cli": { concurrency: 1, startIntervalMs: 2000 },
};
function printUsage(): void {
@@ -78,7 +79,7 @@ Options:
--image <path> Output image path (required in single-image mode)
--batchfile <path> JSON batch file for multi-image generation
--jobs <count> Worker count for batch mode (default: auto, max from config, built-in default 10)
--provider google|openai|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|azure Force provider (auto-detect by default)
--provider google|openai|openrouter|dashscope|zai|minimax|replicate|jimeng|seedream|azure|codex-cli Force provider (auto-detect by default)
-m, --model <id> Model ID
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
--size <WxH> Size (e.g., 1024x1024)
@@ -154,8 +155,13 @@ Environment variables:
AZURE_OPENAI_IMAGE_MODEL Backward-compatible Azure deployment/model alias (defaults to gpt-image-2)
SEEDREAM_BASE_URL Custom Seedream endpoint
BAOYU_IMAGE_GEN_MAX_WORKERS Override batch worker cap
BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY Override provider concurrency
BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY Override provider concurrency (use underscores: BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY)
BAOYU_IMAGE_GEN_<PROVIDER>_START_INTERVAL_MS Override provider start gap in ms
BAOYU_CODEX_IMAGEGEN_BIN Path to codex-imagegen wrapper (default: bundled scripts/codex-imagegen/main.ts; accepts .ts or legacy .sh/binary)
BAOYU_CODEX_IMAGEGEN_CACHE_DIR Enable idempotency cache for codex-cli provider (default: disabled)
BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS Per-attempt codex exec timeout for codex-cli provider (default: 300000)
BAOYU_CODEX_IMAGEGEN_RETRIES Codex-side retry attempts on retryable errors (default: 2)
BAOYU_CODEX_IMAGEGEN_LOG_FILE Append JSONL diagnostic log for codex-cli provider
Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
}
@@ -258,7 +264,8 @@ export function parseArgs(argv: string[]): CliArgs {
v !== "replicate" &&
v !== "jimeng" &&
v !== "seedream" &&
v !== "azure"
v !== "azure" &&
v !== "codex-cli"
) {
throw new Error(`Invalid provider: ${v}`);
}
@@ -430,6 +437,7 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
jimeng: null,
seedream: null,
azure: null,
"codex-cli": null,
};
currentKey = "default_model";
currentProvider = null;
@@ -458,7 +466,8 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
key === "replicate" ||
key === "jimeng" ||
key === "seedream" ||
key === "azure"
key === "azure" ||
key === "codex-cli"
)
) {
config.batch ??= {};
@@ -477,7 +486,8 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
key === "replicate" ||
key === "jimeng" ||
key === "seedream" ||
key === "azure"
key === "azure" ||
key === "codex-cli"
)
) {
const cleaned = value.replace(/['"]/g, "");
@@ -630,10 +640,11 @@ export function getConfiguredProviderRateLimits(
jimeng: { ...DEFAULT_PROVIDER_RATE_LIMITS.jimeng },
seedream: { ...DEFAULT_PROVIDER_RATE_LIMITS.seedream },
azure: { ...DEFAULT_PROVIDER_RATE_LIMITS.azure },
"codex-cli": { ...DEFAULT_PROVIDER_RATE_LIMITS["codex-cli"] },
};
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure"] as Provider[]) {
const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase()}`;
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure", "codex-cli"] as Provider[]) {
const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase().replace(/-/g, "_")}`;
const extendLimit = extendConfig.batch?.provider_limits?.[provider];
configured[provider] = {
concurrency:
@@ -699,10 +710,11 @@ export function detectProvider(args: CliArgs): Provider {
args.provider !== "replicate" &&
args.provider !== "seedream" &&
args.provider !== "minimax" &&
args.provider !== "dashscope"
args.provider !== "dashscope" &&
args.provider !== "codex-cli"
) {
throw new Error(
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider dashscope with a wan2.7 image model, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider dashscope with a wan2.7 image model, --provider seedream for supported Seedream models, --provider minimax for MiniMax subject-reference workflows, or --provider codex-cli (Codex image_gen with references)."
);
}
@@ -839,6 +851,7 @@ async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
if (provider === "jimeng") return (await import("./providers/jimeng")) as ProviderModule;
if (provider === "seedream") return (await import("./providers/seedream")) as ProviderModule;
if (provider === "azure") return (await import("./providers/azure")) as ProviderModule;
if (provider === "codex-cli") return (await import("./providers/codex-cli")) as ProviderModule;
return (await import("./providers/openai")) as ProviderModule;
}
@@ -870,6 +883,7 @@ function getModelForProvider(
if (provider === "jimeng" && extendConfig.default_model.jimeng) return extendConfig.default_model.jimeng;
if (provider === "seedream" && extendConfig.default_model.seedream) return extendConfig.default_model.seedream;
if (provider === "azure" && extendConfig.default_model.azure) return extendConfig.default_model.azure;
if (provider === "codex-cli" && extendConfig.default_model["codex-cli"]) return extendConfig.default_model["codex-cli"];
}
return providerModule.getDefaultModel();
}
@@ -1104,7 +1118,7 @@ async function runBatchTasks(
const acquireProvider = createProviderGate(providerRateLimits);
const workerCount = getWorkerCount(tasks.length, jobs, maxWorkers);
console.error(`Batch mode: ${tasks.length} tasks, ${workerCount} workers, parallel mode enabled.`);
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure"] as Provider[]) {
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "zai", "minimax", "jimeng", "seedream", "azure", "codex-cli"] as Provider[]) {
const limit = providerRateLimits[provider];
console.error(`- ${provider}: concurrency=${limit.concurrency}, startIntervalMs=${limit.startIntervalMs}`);
}
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { CliArgs } from "../types.ts";
import {
getDefaultModel,
getDefaultOutputExtension,
validateArgs,
} from "./codex-cli.ts";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
prompt: null,
promptFiles: [],
imagePath: null,
provider: "codex-cli",
model: null,
aspectRatio: null,
aspectRatioSource: null,
size: null,
quality: "2k",
imageSize: null,
imageSizeSource: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
jobs: null,
json: false,
help: false,
...overrides,
};
}
test("codex-cli defaults to codex-image-gen model and PNG output", () => {
assert.equal(getDefaultModel(), "codex-image-gen");
assert.equal(getDefaultOutputExtension(), ".png");
});
test("codex-cli validateArgs rejects n>1 with a non-retryable message", () => {
assert.throws(
() => validateArgs("codex-image-gen", makeArgs({ n: 2 })),
/supports only n=1/,
);
});
test("codex-cli validateArgs rejects ratio-metadata dialect", () => {
assert.throws(
() => validateArgs("codex-image-gen", makeArgs({ imageApiDialect: "ratio-metadata" })),
/Invalid imageApiDialect/,
);
});
test("codex-cli validateArgs accepts default n=1 with no dialect", () => {
assert.doesNotThrow(() => validateArgs("codex-image-gen", makeArgs()));
});
test("codex-cli validateArgs accepts reference images (Codex image_gen supports refs)", () => {
assert.doesNotThrow(() =>
validateArgs("codex-image-gen", makeArgs({ referenceImages: ["/tmp/a.png", "/tmp/b.png"] })),
);
});
@@ -0,0 +1,197 @@
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import { mkdir, readFile, rm, writeFile, access } from "node:fs/promises";
import { randomBytes } from "node:crypto";
import type { CliArgs } from "../types";
const PROVIDER_FILE = fileURLToPath(import.meta.url);
const SCRIPTS_DIR = path.resolve(path.dirname(PROVIDER_FILE), "..");
const BUNDLED_WRAPPER = path.join(SCRIPTS_DIR, "codex-imagegen", "main.ts");
type WrapperOkResult = {
status: "ok";
path: string;
bytes: number;
elapsed_seconds: number;
thread_id: string | null;
attempts: number;
cached: boolean;
};
type WrapperErrorResult = {
status: "error";
path: string;
bytes: number;
error: string;
error_kind: string;
};
type WrapperResult = WrapperOkResult | WrapperErrorResult;
export function getDefaultModel(): string {
return "codex-image-gen";
}
export function getDefaultOutputExtension(): string {
return ".png";
}
export function validateArgs(_model: string, args: CliArgs): void {
if (args.n > 1) {
throw new Error(
"codex-cli provider supports only n=1 (Codex image_gen returns a single image per call).",
);
}
if (args.imageApiDialect && args.imageApiDialect !== "openai-native") {
throw new Error(
`Invalid imageApiDialect for codex-cli: ${args.imageApiDialect}. codex-cli does not use OpenAI Images API dialects.`,
);
}
}
async function exists(filePath: string): Promise<boolean> {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
async function resolveWrapperPath(): Promise<string> {
const override = process.env.BAOYU_CODEX_IMAGEGEN_BIN;
if (override) {
if (!(await exists(override))) {
throw new Error(
`Invalid BAOYU_CODEX_IMAGEGEN_BIN: ${override} does not exist.`,
);
}
return override;
}
if (await exists(BUNDLED_WRAPPER)) return BUNDLED_WRAPPER;
throw new Error(
`codex-cli wrapper not found at ${BUNDLED_WRAPPER}. ` +
`Reinstall baoyu-image-gen, or set BAOYU_CODEX_IMAGEGEN_BIN to a codex-imagegen main.ts (or .sh) path.`,
);
}
type SpawnResult = {
stdout: string;
stderr: string;
code: number;
};
async function spawnWrapper(wrapperPath: string, cliArgs: string[]): Promise<SpawnResult> {
return new Promise((resolve, reject) => {
const isTs = wrapperPath.endsWith(".ts");
const command = isTs ? "bun" : wrapperPath;
const args = isTs ? [wrapperPath, ...cliArgs] : cliArgs;
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf8");
});
child.stderr.on("data", (chunk: Buffer) => {
const text = chunk.toString("utf8");
stderr += text;
process.stderr.write(text);
});
child.on("error", (err) => reject(err));
child.on("close", (code) => resolve({ stdout, stderr, code: code ?? 1 }));
});
}
function parseWrapperJson(stdout: string): WrapperResult {
const trimmed = stdout.trim();
if (!trimmed) {
throw new Error("Invalid codex-cli response: empty stdout from wrapper.");
}
const lastLine = trimmed.split(/\r?\n/).pop() ?? trimmed;
try {
return JSON.parse(lastLine) as WrapperResult;
} catch (parseErr) {
throw new Error(
`Invalid codex-cli response: could not parse JSON from wrapper stdout (${(parseErr as Error).message}).`,
);
}
}
function parsePositiveInt(value: string | undefined): number | null {
if (!value) return null;
const parsed = parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function getEnvOverride(name: string): string | null {
const value = process.env[name];
return value && value.length > 0 ? value : null;
}
export async function generateImage(
prompt: string,
_model: string,
args: CliArgs,
): Promise<Uint8Array> {
const wrapperPath = await resolveWrapperPath();
const sessionDir = path.join(tmpdir(), "baoyu-image-gen-codex-cli");
await mkdir(sessionDir, { recursive: true });
const token = randomBytes(8).toString("hex");
const tmpOutput = path.join(sessionDir, `out-${token}.png`);
const tmpPrompt = path.join(sessionDir, `prompt-${token}.md`);
await writeFile(tmpPrompt, prompt, "utf8");
const aspect = args.aspectRatio ?? "1:1";
const cliArgs: string[] = [
"--image",
tmpOutput,
"--prompt-file",
tmpPrompt,
"--aspect",
aspect,
];
for (const ref of args.referenceImages) {
cliArgs.push("--ref", path.resolve(ref));
}
const cacheDir = getEnvOverride("BAOYU_CODEX_IMAGEGEN_CACHE_DIR");
if (cacheDir) cliArgs.push("--cache-dir", cacheDir);
const timeoutMs = parsePositiveInt(process.env.BAOYU_CODEX_IMAGEGEN_TIMEOUT_MS);
if (timeoutMs) cliArgs.push("--timeout", String(timeoutMs));
const retries = parsePositiveInt(process.env.BAOYU_CODEX_IMAGEGEN_RETRIES);
if (retries !== null) cliArgs.push("--retries", String(retries));
const logFile = getEnvOverride("BAOYU_CODEX_IMAGEGEN_LOG_FILE");
if (logFile) cliArgs.push("--log-file", logFile);
try {
const spawnResult = await spawnWrapper(wrapperPath, cliArgs);
const parsed = parseWrapperJson(spawnResult.stdout);
if (parsed.status === "error") {
throw new Error(
`Invalid codex-cli result (${parsed.error_kind}): ${parsed.error}`,
);
}
if (spawnResult.code !== 0) {
throw new Error(
`Invalid codex-cli result: wrapper exited with code ${spawnResult.code} despite reporting status=ok.`,
);
}
const bytes = await readFile(parsed.path ?? tmpOutput);
return new Uint8Array(bytes);
} finally {
await Promise.allSettled([
rm(tmpOutput, { force: true }),
rm(tmpPrompt, { force: true }),
]);
}
}
+3 -1
View File
@@ -8,7 +8,8 @@ export type Provider =
| "replicate"
| "jimeng"
| "seedream"
| "azure";
| "azure"
| "codex-cli";
export type Quality = "normal" | "2k";
export type OpenAIImageApiDialect = "openai-native" | "ratio-metadata";
@@ -74,6 +75,7 @@ export type ExtendConfig = {
jimeng: string | null;
seedream: string | null;
azure: string | null;
"codex-cli": string | null;
};
batch?: {
max_workers?: number | null;
+4 -2
View File
@@ -1,7 +1,7 @@
---
name: baoyu-infographic
description: Generate professional infographics with 21 layout types and 22 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
version: 1.117.3
version: 1.117.4
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-infographic
@@ -29,6 +29,7 @@ When this skill needs to render an image, resolve the backend in this order:
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-image-gen`) unless the user has explicitly pinned a different `preferred_image_backend`.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime exposes no native `imagegen` skill but the `codex` CLI is on `PATH` with an active `codex login`, route through `baoyu-image-gen --provider codex-cli` (preferred), or — if baoyu-image-gen is unavailable — invoke the bundled wrapper directly. Details, parameters, and the runtime-discovery procedure live in [references/codex-imagegen.md](references/codex-imagegen.md) — load that file only when this branch is selected.
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-image-gen`), use it.
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
@@ -296,7 +297,8 @@ Combine:
2. Ensure the full final prompt is persisted at `prompts/infographic.md` (already written in Step 5) BEFORE invoking the backend — the file is the reproducibility record.
3. **Check for existing file**: Before generating, check if `infographic.png` exists
- If exists: Rename to `infographic-backup-YYYYMMDD-HHMMSS.png`
4. Call the chosen backend with the prompt file and output path
4. Call the chosen backend with the prompt file and output path.
- **`codex-imagegen` invocation**: when the rule resolves to `codex-imagegen`, see [references/codex-imagegen.md](references/codex-imagegen.md) for the invocation contract (preferred `baoyu-image-gen --provider codex-cli` path, runtime wrapper discovery, parameter notes, stdout schema, batch semantics).
5. On failure, auto-retry once
Text correction policy:
@@ -0,0 +1,65 @@
# `codex-imagegen` Wrapper Invocation
Load this reference only when the [Image Generation Tools](../SKILL.md#image-generation-tools) rule has resolved to `codex-imagegen` — i.e., the current runtime exposes no native `imagegen` skill but `codex` CLI is on `PATH` with an active `codex login`.
## Preferred path: route through `baoyu-image-gen`
If the `baoyu-image-gen` skill is available in this runtime, **always** invoke through it rather than calling the wrapper directly. It handles retry/cache/batch/EXTEND.md preferences uniformly with every other provider.
```bash
${BUN_X} <baoyu-image-gen-base>/scripts/main.ts \
--provider codex-cli \
--image <ABSOLUTE_output> \
--promptfiles <ABSOLUTE_prompts/infographic.md> \
--ar <ratio> \
[--ref <ABSOLUTE_file>]...
```
Resolve `<baoyu-image-gen-base>` the same way you resolve any sibling skill — through your runtime's skill registry (`Skill` tool, plugin marketplace, or `$HOME/.baoyu-skills/baoyu-image-gen/`).
## Fallback: spawn the wrapper directly
Only when `baoyu-image-gen` is NOT installed in the current runtime. Discover the wrapper's location at runtime — do NOT hard-code `../../packages/...` from this skill:
1. **Honor explicit override**: if `$BAOYU_CODEX_IMAGEGEN_BIN` is set and points to a real file, use that path. It may be `.ts` (spawn `bun <path>`) or `.sh`/binary (spawn directly).
2. **Search the plugin root**: walk up from this skill's directory looking for `packages/baoyu-codex-imagegen/src/main.ts`. If found, that is the wrapper. Spawn it with `bun`.
3. **Last resort**: tell the user that `codex-imagegen` is not available in this runtime and ask whether to install the `baoyu-skills` plugin (or set `BAOYU_CODEX_IMAGEGEN_BIN`) or pick another backend.
Once located, the invocation shape is:
```bash
bun <WRAPPER>/main.ts \
--image <ABSOLUTE_output> \
--prompt-file <ABSOLUTE_prompts/infographic.md> \
--aspect <ratio> \
[--ref <ABSOLUTE_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <ABSOLUTE_jsonl_log_path>]
```
If `bun` is missing, `npx -y bun <WRAPPER>/main.ts ...` works as a fallback.
## Parameter notes
- **All filesystem inputs** are auto-resolved against `process.cwd()` when relative, but agents should pass absolute paths to be robust against cwd drift.
- **`--timeout`** defaults to `300000` (5 min) per `codex exec` attempt. Raise (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
- **`--cache-dir`** is off by default. Enable for repeatable runs to skip redundant generations of the same prompt+aspect+refs.
- **Authentication**: the wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` is read or sent.
## Stdout contract
Single JSON line:
- Success: `{"status":"ok","path":"...","bytes":N,"elapsed_seconds":N,"thread_id":"...","attempts":N,"cached":bool,...}`
- Failure: `{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}`
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
On retryable errors (timeout, spawn_failed, no_image_gen_tool_use, output_missing, invalid_png, agent_refused), ask the user whether to retry or fall back to another backend.
## Batch semantics
- Codex `image_gen` returns **one image per call** (`n=1` only). Multi-image jobs must dispatch one wrapper call per image.
- The wrapper does NOT accept a `--sessionId` flag. Chain/scene consistency must come from `--ref` reference images.
- `--size` and `--quality` are silently ignored — Codex picks pixel dimensions from `--aspect`.
+3 -1
View File
@@ -1,7 +1,7 @@
---
name: baoyu-slide-deck
description: Generates professional slide deck images from content. Creates outlines with style instructions, then generates individual slide images. Use when user asks to "create slides", "make a presentation", "generate deck", "slide deck", or "PPT".
version: 1.117.3
version: 1.117.4
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-slide-deck
@@ -33,6 +33,7 @@ When this skill needs to render an image, resolve the backend in this order:
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-image-gen`) unless the user has explicitly pinned a different `preferred_image_backend`.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime exposes no native `imagegen` skill but the `codex` CLI is on `PATH` with an active `codex login`, route through `baoyu-image-gen --provider codex-cli` (preferred), or — if baoyu-image-gen is unavailable — invoke the bundled wrapper directly. Details, parameters, and the runtime-discovery procedure live in [references/codex-imagegen.md](references/codex-imagegen.md) — load that file only when this branch is selected.
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-image-gen`), use it.
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
@@ -297,6 +298,7 @@ Display the prompts index (`# | Filename | Slide Title`) and ask: proceed / edit
### Step 7: Generate Images
1. Resolve the image backend via the Image Generation Tools rule at the top — ask once if multiple are installed.
- **`codex-imagegen` invocation**: when the rule resolves to `codex-imagegen`, see [references/codex-imagegen.md](references/codex-imagegen.md) for the invocation contract (preferred `baoyu-image-gen --provider codex-cli` path, runtime wrapper discovery, parameter notes, stdout schema, batch semantics — n=1 per call so slide batches must dispatch one wrapper call per slide).
2. Confirm every `prompts/NN-slide-{slug}.md` exists (hard requirement; prompt files are the reproducibility record regardless of backend).
3. Session ID: `slides-{topic-slug}-{timestamp}` — pass to the backend only if it supports sessions.
4. Build a task list for selected slides with each slide's prompt file, output PNG path, aspect ratio, session ID, and verified direct references.
@@ -0,0 +1,65 @@
# `codex-imagegen` Wrapper Invocation
Load this reference only when the [Image Generation Tools](../SKILL.md#image-generation-tools) rule has resolved to `codex-imagegen` — i.e., the current runtime exposes no native `imagegen` skill but `codex` CLI is on `PATH` with an active `codex login`.
## Preferred path: route through `baoyu-image-gen`
If the `baoyu-image-gen` skill is available in this runtime, **always** invoke through it rather than calling the wrapper directly. It handles retry/cache/batch/EXTEND.md preferences uniformly with every other provider.
```bash
${BUN_X} <baoyu-image-gen-base>/scripts/main.ts \
--provider codex-cli \
--image <ABSOLUTE_output> \
--promptfiles <ABSOLUTE_prompts/NN-slide-{slug}.md> \
--ar <ratio> \
[--ref <ABSOLUTE_file>]...
```
Resolve `<baoyu-image-gen-base>` the same way you resolve any sibling skill — through your runtime's skill registry (`Skill` tool, plugin marketplace, or `$HOME/.baoyu-skills/baoyu-image-gen/`).
## Fallback: spawn the wrapper directly
Only when `baoyu-image-gen` is NOT installed in the current runtime. Discover the wrapper's location at runtime — do NOT hard-code `../../packages/...` from this skill:
1. **Honor explicit override**: if `$BAOYU_CODEX_IMAGEGEN_BIN` is set and points to a real file, use that path. It may be `.ts` (spawn `bun <path>`) or `.sh`/binary (spawn directly).
2. **Search the plugin root**: walk up from this skill's directory looking for `packages/baoyu-codex-imagegen/src/main.ts`. If found, that is the wrapper. Spawn it with `bun`.
3. **Last resort**: tell the user that `codex-imagegen` is not available in this runtime and ask whether to install the `baoyu-skills` plugin (or set `BAOYU_CODEX_IMAGEGEN_BIN`) or pick another backend.
Once located, the invocation shape is:
```bash
bun <WRAPPER>/main.ts \
--image <ABSOLUTE_output> \
--prompt-file <ABSOLUTE_prompts/NN-slide-{slug}.md> \
--aspect <ratio> \
[--ref <ABSOLUTE_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <ABSOLUTE_jsonl_log_path>]
```
If `bun` is missing, `npx -y bun <WRAPPER>/main.ts ...` works as a fallback.
## Parameter notes
- **All filesystem inputs** are auto-resolved against `process.cwd()` when relative, but agents should pass absolute paths to be robust against cwd drift.
- **`--timeout`** defaults to `300000` (5 min) per `codex exec` attempt. Raise (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
- **`--cache-dir`** is off by default. Enable for repeatable runs to skip redundant generations of the same prompt+aspect+refs.
- **Authentication**: the wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` is read or sent.
## Stdout contract
Single JSON line:
- Success: `{"status":"ok","path":"...","bytes":N,"elapsed_seconds":N,"thread_id":"...","attempts":N,"cached":bool,...}`
- Failure: `{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}`
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
On retryable errors (timeout, spawn_failed, no_image_gen_tool_use, output_missing, invalid_png, agent_refused), ask the user whether to retry or fall back to another backend.
## Batch semantics
- Codex `image_gen` returns **one image per call** (`n=1` only). Multi-image jobs must dispatch one wrapper call per image.
- The wrapper does NOT accept a `--sessionId` flag. Chain/scene consistency must come from `--ref` reference images.
- `--size` and `--quality` are silently ignored — Codex picks pixel dimensions from `--aspect`.
+4 -1
View File
@@ -1,7 +1,7 @@
---
name: baoyu-xhs-images
description: Generates infographic image card series with 12 visual styles, 8 layouts, and 3 color palettes. Breaks content into 1-10 cartoon-style image cards optimized for social media engagement. Use when user mentions "小红书图片", "小红书种草", "小绿书", "微信图文", "微信贴图", "image cards", "图片卡片", baoyu-xhs-images, or wants social media infographic series.
version: 2.0.0
version: 2.0.1
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-xhs-images
@@ -29,6 +29,7 @@ When this skill needs to render an image, resolve the backend in this order:
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-image-gen`) unless the user has explicitly pinned a different `preferred_image_backend`.
- **Codex via `codex exec` (`codex-imagegen`)** — if the current runtime exposes no native `imagegen` skill but the `codex` CLI is on `PATH` with an active `codex login`, route through `baoyu-image-gen --provider codex-cli` (preferred), or — if baoyu-image-gen is unavailable — invoke the bundled wrapper directly. Details, parameters, and the runtime-discovery procedure live in [references/codex-imagegen.md](references/codex-imagegen.md) — load that file only when this branch is selected.
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-image-gen`), use it.
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
@@ -387,6 +388,8 @@ See `references/config/watermark-guide.md`.
**Backend selection**: per the Image Generation Tools rule at the top — use whatever is available, ask once if multiple, before any generation. Under `--yes`, use the EXTEND.md preference and fall back to the first available backend. Prompt files MUST exist before invoking any backend.
**`codex-imagegen` invocation**: when the rule resolves to `codex-imagegen`, see [references/codex-imagegen.md](references/codex-imagegen.md) for the invocation contract (preferred `baoyu-image-gen --provider codex-cli` path, runtime wrapper discovery, parameter notes, stdout schema, batch semantics — n=1 per call so card batches must dispatch one wrapper call per card; the wrapper does NOT accept `--sessionId`, so chain consistency must come from `--ref` per Step 3 above).
**Session ID** (if the backend supports `--sessionId`): use `cards-{topic-slug}-{timestamp}` for every image; combined with the ref chain this gives maximum consistency.
### Step 4: Completion Report
@@ -0,0 +1,65 @@
# `codex-imagegen` Wrapper Invocation
Load this reference only when the [Image Generation Tools](../SKILL.md#image-generation-tools) rule has resolved to `codex-imagegen` — i.e., the current runtime exposes no native `imagegen` skill but `codex` CLI is on `PATH` with an active `codex login`.
## Preferred path: route through `baoyu-image-gen`
If the `baoyu-image-gen` skill is available in this runtime, **always** invoke through it rather than calling the wrapper directly. It handles retry/cache/batch/EXTEND.md preferences uniformly with every other provider.
```bash
${BUN_X} <baoyu-image-gen-base>/scripts/main.ts \
--provider codex-cli \
--image <ABSOLUTE_output> \
--promptfiles <ABSOLUTE_prompts/NN-{type}-[slug].md> \
--ar <ratio> \
[--ref <ABSOLUTE_file>]...
```
Resolve `<baoyu-image-gen-base>` the same way you resolve any sibling skill — through your runtime's skill registry (`Skill` tool, plugin marketplace, or `$HOME/.baoyu-skills/baoyu-image-gen/`).
## Fallback: spawn the wrapper directly
Only when `baoyu-image-gen` is NOT installed in the current runtime. Discover the wrapper's location at runtime — do NOT hard-code `../../packages/...` from this skill:
1. **Honor explicit override**: if `$BAOYU_CODEX_IMAGEGEN_BIN` is set and points to a real file, use that path. It may be `.ts` (spawn `bun <path>`) or `.sh`/binary (spawn directly).
2. **Search the plugin root**: walk up from this skill's directory looking for `packages/baoyu-codex-imagegen/src/main.ts`. If found, that is the wrapper. Spawn it with `bun`.
3. **Last resort**: tell the user that `codex-imagegen` is not available in this runtime and ask whether to install the `baoyu-skills` plugin (or set `BAOYU_CODEX_IMAGEGEN_BIN`) or pick another backend.
Once located, the invocation shape is:
```bash
bun <WRAPPER>/main.ts \
--image <ABSOLUTE_output> \
--prompt-file <ABSOLUTE_prompts/NN-{type}-[slug].md> \
--aspect <ratio> \
[--ref <ABSOLUTE_file>]... \
[--timeout <ms>] \
[--cache-dir ~/.cache/baoyu-codex-imagegen] \
[--log-file <ABSOLUTE_jsonl_log_path>]
```
If `bun` is missing, `npx -y bun <WRAPPER>/main.ts ...` works as a fallback.
## Parameter notes
- **All filesystem inputs** are auto-resolved against `process.cwd()` when relative, but agents should pass absolute paths to be robust against cwd drift.
- **`--timeout`** defaults to `300000` (5 min) per `codex exec` attempt. Raise (e.g. `--timeout 600000` for 10 min) on slow networks or large prompts.
- **`--cache-dir`** is off by default. Enable for repeatable runs to skip redundant generations of the same prompt+aspect+refs.
- **Authentication**: the wrapper uses the user's Codex subscription — no `OPENAI_API_KEY` is read or sent.
## Stdout contract
Single JSON line:
- Success: `{"status":"ok","path":"...","bytes":N,"elapsed_seconds":N,"thread_id":"...","attempts":N,"cached":bool,...}`
- Failure: `{"status":"error","path":"...","bytes":0,"error":"...","error_kind":"..."}`
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
On retryable errors (timeout, spawn_failed, no_image_gen_tool_use, output_missing, invalid_png, agent_refused), ask the user whether to retry or fall back to another backend.
## Batch semantics
- Codex `image_gen` returns **one image per call** (`n=1` only). Multi-image jobs must dispatch one wrapper call per image.
- The wrapper does NOT accept a `--sessionId` flag. Chain/scene consistency must come from `--ref` reference images.
- `--size` and `--quality` are silently ignored — Codex picks pixel dimensions from `--aspect`.