mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-14 22:59:46 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1c0ff7c02 | |||
| 860dd36bb6 | |||
| aab0e28823 | |||
| 95cdad2a2a | |||
| 8838161729 | |||
| 49e9c46ca4 | |||
| 23fac03691 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "2.1.0"
|
||||
"version": "2.2.1"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
- 5–10× 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"
|
||||
}
|
||||
}
|
||||
Regular → Executable
+1
@@ -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";
|
||||
@@ -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" "$@"
|
||||
Executable
+59
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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 5–10× 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: 5–10× 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 5–10× 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
@@ -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 };
|
||||
}
|
||||
@@ -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 }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user