refactor(codex-imagegen): extract backend to packages/baoyu-codex-imagegen workspace

Move the codex-imagegen wrapper out of scripts/ into its own workspace
package alongside baoyu-md, baoyu-chrome-cdp, and baoyu-fetch. Drop the
bash shim — src/main.ts now carries a `#\!/usr/bin/env bun` shebang and
serves as the sole entrypoint. Add scripts/sync-codex-imagegen.sh so
skills/baoyu-image-gen/scripts/codex-imagegen/ can be regenerated from
packages/baoyu-codex-imagegen/src/ for skill self-containment. Update
CLAUDE.md, docs/codex-imagegen-backend.md, and the CI workflow paths
accordingly.
This commit is contained in:
Jim Liu 宝玉
2026-05-25 00:18:16 -05:00
parent 66160abb9f
commit 23fac03691
17 changed files with 252 additions and 54 deletions
+5 -7
View File
@@ -3,13 +3,11 @@ name: codex-imagegen tests
on:
push:
paths:
- 'scripts/codex-imagegen/**'
- 'scripts/codex-imagegen.sh'
- 'packages/baoyu-codex-imagegen/**'
- '.github/workflows/codex-imagegen-tests.yml'
pull_request:
paths:
- 'scripts/codex-imagegen/**'
- 'scripts/codex-imagegen.sh'
- 'packages/baoyu-codex-imagegen/**'
- '.github/workflows/codex-imagegen-tests.yml'
workflow_dispatch:
@@ -30,11 +28,11 @@ jobs:
run: bun --version
- name: Run unit tests
working-directory: scripts/codex-imagegen
working-directory: packages/baoyu-codex-imagegen
run: bun test
- name: Bundle smoke test (catches import/syntax errors)
run: bun build --target=node scripts/codex-imagegen/main.ts --outfile /tmp/main-build.js
run: bun build --target=node packages/baoyu-codex-imagegen/src/main.ts --outfile /tmp/main-build.js
- name: Help output smoke test
run: bun scripts/codex-imagegen/main.ts --help
run: bun packages/baoyu-codex-imagegen/src/main.ts --help
+6 -4
View File
@@ -68,19 +68,21 @@ Skills that render images MUST declare the backend-selection convention **inline
### `codex-imagegen` Backend
A backend for non-Codex runtimes (e.g., Claude Code) that generates images by spawning `codex exec --json --sandbox danger-full-access` and delegating to Codex CLI's built-in `image_gen` tool. Uses the user's Codex subscription — no `OPENAI_API_KEY` required.
A backend for non-Codex runtimes (e.g., Claude Code) that generates images by spawning `codex exec --json --sandbox danger-full-access` and delegating to Codex CLI's built-in `image_gen` tool. Uses the user's Codex subscription — no `OPENAI_API_KEY` required. Lives under [packages/baoyu-codex-imagegen](packages/baoyu-codex-imagegen) so it follows the same workspace layout as other shared packages.
Invoke via:
Invoke via (TS entrypoint with `#!/usr/bin/env bun` shebang):
```bash
./scripts/codex-imagegen.sh \
bun packages/baoyu-codex-imagegen/src/main.ts \
--image <output.png> \
--prompt-file prompts/01-cover.md \
--aspect 16:9 \
--cache-dir ~/.cache/baoyu-codex-imagegen
```
Stdout emits a single JSON line: `{"status":"ok","path":...,"bytes":N,...}`. On failure, `{"status":"error","error_kind":...}`. Skills route here by setting `preferred_image_backend: codex-imagegen` in EXTEND.md. Full reference: [docs/codex-imagegen-backend.md](docs/codex-imagegen-backend.md).
Without bun installed: `npx -y bun packages/baoyu-codex-imagegen/src/main.ts …`.
Stdout emits a single JSON line: `{"status":"ok","path":...,"bytes":N,...}`. On failure, `{"status":"error","error_kind":...}`. Skills route here by setting `preferred_image_backend: codex-imagegen` in EXTEND.md, or by running `baoyu-image-gen --provider codex-cli` (which spawns the same wrapper internally). Full reference: [docs/codex-imagegen-backend.md](docs/codex-imagegen-backend.md).
## Release Process
+40 -24
View File
@@ -35,13 +35,13 @@ codex login # signs in with your OpenAI account (subscription)
codex --version # confirm >= 0.130
```
`bun` is preferred for running the wrapper. On macOS:
`bun` is required for running the wrapper. On macOS:
```bash
brew install oven-sh/bun/bun
```
If `bun` is missing, the shell entrypoint falls back to `npx -y bun`.
If `bun` is not on `PATH`, fall back to `npx -y bun packages/baoyu-codex-imagegen/src/main.ts …`.
## Usage
@@ -49,20 +49,32 @@ If `bun` is missing, the shell entrypoint falls back to `npx -y bun`.
```bash
# Inline prompt
./scripts/codex-imagegen.sh \
bun packages/baoyu-codex-imagegen/src/main.ts \
--image /tmp/cat.png \
--prompt "A friendly orange cat, watercolor"
# Prompt from file
./scripts/codex-imagegen.sh \
bun packages/baoyu-codex-imagegen/src/main.ts \
--image cover.png \
--prompt-file prompts/01-cover.md \
--aspect 16:9
# Verbose mode for debugging
./scripts/codex-imagegen.sh -v --image dog.png --prompt "A corgi" --aspect 1:1
bun packages/baoyu-codex-imagegen/src/main.ts -v --image dog.png --prompt "A corgi" --aspect 1:1
```
### Through `baoyu-image-gen`
```bash
${BUN_X} skills/baoyu-image-gen/scripts/main.ts \
--provider codex-cli \
--prompt "A friendly orange cat, watercolor" \
--image /tmp/cat.png \
--ar 1:1
```
The `codex-cli` provider spawns the bundled `codex-imagegen` TS entrypoint internally and surfaces its retry/cache machinery through baoyu-image-gen's standard CLI + batch flow.
On success, stdout emits a single JSON line:
```json
@@ -80,7 +92,7 @@ Image-generating skills (e.g., `baoyu-cover-image`, `baoyu-article-illustrator`)
preferred_image_backend: codex-imagegen
```
When the LLM runs the skill, it reads the preference and — guided by the `### codex-imagegen Backend` section in `CLAUDE.md` — invokes `scripts/codex-imagegen.sh`.
When the LLM runs the skill, it reads the preference and — guided by the `### codex-imagegen Backend` section in `CLAUDE.md` — invokes `bun packages/baoyu-codex-imagegen/src/main.ts`.
> **Note**: The integration is mediated by the LLM reading `CLAUDE.md`. It is not a hard binding. If a skill does not route to the backend automatically, mentioning it explicitly in the prompt works.
@@ -191,24 +203,26 @@ On failure, exit code is `1` and the JSON contains `error` and `error_kind`:
## Architecture
```
scripts/codex-imagegen.sh # thin bash entrypoint
scripts/codex-imagegen/
├── main.ts # parseArgs → cache → lock → retry loop → emit JSON
├── types.ts # CliOptions, GenerateResult, GenError, ErrorKind
├── spawn.ts # spawn codex exec --json --sandbox danger-full-access
├── parser.ts # parse JSONL event stream → toolCalls, usage, thread_id
├── validator.ts # verify image_gen invocation + PNG magic + file size
├── cache.ts # cacheKey(sha256), FileLock, lookup/store
├── logger.ts # JsonLogger (verbose stderr + JSONL file)
├── parser.test.ts
├── cache.test.ts
└── validator.test.ts
packages/baoyu-codex-imagegen/
├── src/
├── main.ts # parseArgs → cache → lock → retry loop → emit JSON (`#!/usr/bin/env bun`)
├── types.ts # CliOptions, GenerateResult, GenError, ErrorKind
├── spawn.ts # spawn codex exec --json --sandbox danger-full-access
├── parser.ts # parse JSONL event stream → toolCalls, usage, thread_id
├── validator.ts # verify image_gen invocation + PNG magic + file size
├── cache.ts # cacheKey(sha256), FileLock, lookup/store
├── logger.ts # JsonLogger (verbose stderr + JSONL file)
├── parser.test.ts
├── cache.test.ts
└── validator.test.ts
├── package.json # workspace package: `bin` → `src/main.ts`, no build step
└── README.md
```
Run tests:
```bash
cd scripts/codex-imagegen && bun test
cd packages/baoyu-codex-imagegen && bun test
```
## Internal Flow
@@ -216,7 +230,7 @@ cd scripts/codex-imagegen && bun test
```mermaid
flowchart LR
CC[Claude Code / any caller]
WRAPPER[scripts/codex-imagegen.sh]
WRAPPER[bun packages/baoyu-codex-imagegen/<br/>src/main.ts]
CODEX["codex exec --json<br/>--sandbox danger-full-access"]
AGENT[Codex agent]
TOOL[image_gen built-in tool]
@@ -239,18 +253,20 @@ flowchart LR
## Design Decisions
1. **Bash entrypoint + TypeScript implementation** — the shell wrapper picks the runtime (`bun` preferred, falling back to `npx -y bun`); TypeScript handles the orchestration, parsing, retry, cache, and logging. This mirrors the project's existing `scripts/*.mjs` and `skills/<skill>/scripts/main.ts` pattern.
1. **Pure TypeScript entrypoint**`src/main.ts` carries a `#!/usr/bin/env bun` shebang and is the sole entry. There is no shell shim: callers either invoke `bun src/main.ts …` directly, run the file as an executable (when `bun` is on `PATH`), or fall back to `npx -y bun src/main.ts …`. This matches the project's `skills/<skill>/scripts/main.ts` convention.
2. **`--sandbox danger-full-access`** — necessary so the spawned agent can `cp`/`mv` the rendered PNG out of `$CODEX_HOME/generated_images/` to the user-specified path. Standard sandboxes block this.
3. **Parse the JSONL event stream** — the final `agent_message` and intermediate `command_execution` events let the wrapper verify what actually happened (was `image_gen` called? did `cp` reach the right destination?), which is far more reliable than scraping freeform stdout.
4. **Infrastructure, not a skill** — this backend is a CLI utility that skills route to via `preferred_image_backend`. It belongs in `scripts/`, not `skills/`, because it has no `SKILL.md` and is never loaded directly by an agent.
4. **Shared package, not a skill** — this backend is a CLI utility that skills route to via `preferred_image_backend` and that `baoyu-image-gen --provider codex-cli` spawns internally. It lives under `packages/` alongside the other shared workspaces (`baoyu-md`, `baoyu-chrome-cdp`, `baoyu-fetch`) because it has no `SKILL.md` and is never loaded directly by an agent.
5. **File lock instead of internal queue** — keeps the implementation small and works across multiple shell sessions or processes invoking the same wrapper concurrently.
## Related Files
| File | Role |
|------|------|
| `scripts/codex-imagegen.sh` | CLI entrypoint |
| `scripts/codex-imagegen/` | TypeScript implementation |
| `packages/baoyu-codex-imagegen/src/main.ts` | TypeScript CLI entrypoint (`#!/usr/bin/env bun`) |
| `packages/baoyu-codex-imagegen/src/` | TypeScript implementation |
| `packages/baoyu-codex-imagegen/package.json` | Workspace manifest |
| `skills/baoyu-image-gen/scripts/providers/codex-cli.ts` | Provider adapter that lets `baoyu-image-gen --provider codex-cli` spawn this wrapper |
| `docs/codex-imagegen-backend.md` | This document |
| `CLAUDE.md` | Tells LLMs how to invoke this backend |
| `.github/workflows/codex-imagegen-tests.yml` | CI unit tests |
+102
View File
@@ -0,0 +1,102 @@
# baoyu-codex-imagegen
Generate images via Codex CLI's built-in `image_gen` tool from non-Codex runtimes (e.g., Claude Code). The wrapper spawns `codex exec --json` and lets the user's existing Codex subscription drive image generation — **no `OPENAI_API_KEY` required**.
This package implements the `preferred_image_backend: codex-imagegen` config key referenced across the `baoyu-skills` plugin and is the engine behind `baoyu-image-gen --provider codex-cli`.
## Layout
```
packages/baoyu-codex-imagegen/
├── src/
│ ├── main.ts # CLI orchestrator (executable via `#!/usr/bin/env bun`)
│ ├── spawn.ts # codex exec child-process wrapper
│ ├── parser.ts # JSONL event-stream parser
│ ├── validator.ts # Output PNG / image_gen-invocation verification
│ ├── cache.ts # SHA256 idempotency cache + file lock
│ ├── logger.ts # Structured JSONL logging
│ ├── types.ts # Shared types and `GenError`
│ └── *.test.ts # Bun unit tests
└── package.json # `bin` points to `src/main.ts`
```
## Prerequisites
```bash
npm install -g @openai/codex
codex login # signs in with your OpenAI account (subscription)
codex --version # confirm >= 0.130
```
`bun` is required for running the wrapper:
```bash
brew install oven-sh/bun/bun
```
If `bun` is not on `PATH`, `npx -y bun src/main.ts …` works as a fallback.
## Usage
```bash
# Inline prompt (executes via shebang once bun is on PATH)
./src/main.ts \
--image /tmp/cat.png \
--prompt "A friendly orange cat, watercolor"
# Or invoke bun explicitly
bun src/main.ts \
--image cover.png \
--prompt-file prompts/01-cover.md \
--aspect 16:9 \
--cache-dir ~/.cache/baoyu-codex-imagegen
# Without bun installed
npx -y bun src/main.ts --image cover.png --prompt "..."
```
Stdout emits a single JSON line:
```json
{"status":"ok","path":"…","bytes":1234567,"elapsed_seconds":62,"thread_id":"…","attempts":1,"cached":false,"usage":{}}
```
On failure:
```json
{"status":"error","path":"…","bytes":0,"error":"…","error_kind":"timeout"}
```
`error_kind` values: `codex_not_installed`, `invalid_args`, `prompt_file_missing`, `spawn_failed`, `timeout`, `no_image_gen_tool_use`, `output_missing`, `invalid_png`, `agent_refused`, `lock_busy`.
## Options
| Flag | Description |
|---|---|
| `--image <path>` | Output PNG path (required) |
| `--prompt <text>` | Prompt text |
| `--prompt-file <path>` | Read prompt from file (mutually exclusive with `--prompt`) |
| `--aspect <ratio>` | Aspect ratio (`1:1`, `16:9`, `9:16`, `4:3`, `2.35:1`). Default: `1:1` |
| `--ref <file>` | Reference image (repeatable) |
| `--timeout <ms>` | Codex exec timeout in ms. Default: `300000` |
| `--retries <n>` | Retry attempts on retryable errors. Default: `2` |
| `--retry-delay <ms>` | Base retry delay (exponential). Default: `1500` |
| `--cache-dir <path>` | Enable idempotency cache. Disabled by default. |
| `--log-file <path>` | Append structured JSONL log |
| `-v, --verbose` | Verbose stderr logging |
| `-h, --help` | Show help |
## Test
```bash
cd packages/baoyu-codex-imagegen
bun test
```
## Trade-offs
- 510× slower than direct OpenAI API calls (except on cache hits)
- Uses your Codex subscription — programmatic use of `codex exec` falls into the same terms as interactive use
- Requires `codex` CLI and active login session
See [`docs/codex-imagegen-backend.md`](../../docs/codex-imagegen-backend.md) for the full background.
@@ -0,0 +1,39 @@
{
"name": "baoyu-codex-imagegen",
"version": "0.1.0",
"type": "module",
"description": "Generate images via Codex CLI's built-in image_gen tool from non-Codex runtimes (Claude Code, Hermes, …).",
"bin": {
"codex-imagegen": "./src/main.ts"
},
"files": [
"src/**/*.ts",
"!src/**/*.test.ts"
],
"exports": {
".": {
"types": "./src/main.ts",
"default": "./src/main.ts"
},
"./src/*": "./src/*"
},
"scripts": {
"test": "bun test",
"smoke": "bun src/main.ts --help"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JimLiu/baoyu-skills.git",
"directory": "packages/baoyu-codex-imagegen"
},
"bugs": {
"url": "https://github.com/JimLiu/baoyu-skills/issues"
},
"homepage": "https://github.com/JimLiu/baoyu-skills/tree/main/packages/baoyu-codex-imagegen#readme",
"publishConfig": {
"access": "public"
},
"engines": {
"bun": ">=1.2.0"
}
}
@@ -1,3 +1,4 @@
#!/usr/bin/env bun
import { readFile, mkdir, copyFile, stat } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
# codex-imagegen: generate images via Codex CLI's built-in image_gen tool
# Thin shell wrapper — implementation in codex-imagegen/main.ts (Bun TypeScript)
#
# Usage: ./codex-imagegen.sh --help
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if command -v bun &>/dev/null; then
BUN_X="bun"
elif command -v npx &>/dev/null; then
BUN_X="npx -y bun"
else
echo "Error: bun or npx required. Install: brew install oven-sh/bun/bun" >&2
exit 1
fi
exec $BUN_X "$SCRIPT_DIR/codex-imagegen/main.ts" "$@"
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# Sync packages/baoyu-codex-imagegen/src/*.ts (excluding tests) into
# skills/baoyu-image-gen/scripts/codex-imagegen/ so the skill stays
# self-contained (no `../../../../packages/...` lookups at runtime).
#
# Run this whenever packages/baoyu-codex-imagegen/src/ changes,
# and always before tagging a release.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC_DIR="$REPO_ROOT/packages/baoyu-codex-imagegen/src"
DST_DIR="$REPO_ROOT/skills/baoyu-image-gen/scripts/codex-imagegen"
if [[ ! -d "$SRC_DIR" ]]; then
echo "Error: source dir missing: $SRC_DIR" >&2
exit 1
fi
mkdir -p "$DST_DIR"
changed=0
for f in cache.ts logger.ts main.ts parser.ts spawn.ts types.ts validator.ts; do
src="$SRC_DIR/$f"
dst="$DST_DIR/$f"
if [[ ! -f "$src" ]]; then
echo "Error: missing source file: $src" >&2
exit 1
fi
if [[ ! -f "$dst" ]] || ! cmp -s "$src" "$dst"; then
cp "$src" "$dst"
echo "synced: $f"
changed=$((changed + 1))
fi
done
chmod +x "$DST_DIR/main.ts"
# Drop any stale .ts files in DST that don't exist in SRC.
for dst in "$DST_DIR"/*.ts; do
[[ -e "$dst" ]] || continue
name="$(basename "$dst")"
case "$name" in
*.test.ts) rm -f "$dst"; echo "removed test artifact: $name" ;;
*)
if [[ ! -f "$SRC_DIR/$name" ]]; then
rm -f "$dst"
echo "removed stale: $name"
changed=$((changed + 1))
fi
;;
esac
done
if [[ "$changed" -eq 0 ]]; then
echo "codex-imagegen sync: up to date"
else
echo "codex-imagegen sync: $changed file(s) updated"
fi