-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add managed image generation (/codex:imagegen) #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9f1b9d1
30c0ca4
dba88a5
95af638
4d64c02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,16 @@ | ||
| # Changelog | ||
|
|
||
| ## 1.0.5 | ||
|
|
||
| - Add managed image generation: `/codex:imagegen` plus an `imagegen` companion | ||
| subcommand, run over the same single serialized app-server connection the | ||
| plugin already uses for code tasks. Flags: `--out`, `--image <ref[,ref...]>` | ||
| (edit from reference images), `--background`, `--force`, `--model`. The path | ||
| captures the `imageGeneration` item the moment it arrives, writes the bytes to | ||
| `--out`, then interrupts the turn so the model's post-image shell tail never | ||
| runs. Reusing the one serialized connection also avoids the 403/429 contention | ||
| that bare parallel Codex CLI calls caused. Fixes #356. | ||
|
|
||
| ## 1.0.0 | ||
|
|
||
| - Initial version of the Codex plugin for Claude Code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| --- | ||
| description: Generate an image with Codex (managed and serialized) and save it to a file | ||
| argument-hint: '[--out <path>] [--force] [--image <ref[,ref...]>] [--background] [--model <model>] [what the image should be]' | ||
| allowed-tools: Bash(node:*) | ||
| --- | ||
|
|
||
| Generate an image through the Codex companion's managed, serialized image path and return its output verbatim. | ||
|
|
||
| Raw user request: | ||
| $ARGUMENTS | ||
|
|
||
| Run one `Bash` call: | ||
|
|
||
| ```bash | ||
| node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" imagegen "$ARGUMENTS" | ||
| ``` | ||
|
|
||
| How it works: | ||
|
|
||
| - `imagegen` runs a single Codex app-server turn, captures the generated image the moment it is ready, writes it to `--out` (or reports the Codex copy under `~/.codex/generated_images/`), and interrupts the turn so the model's post-image steps do not run. | ||
| - It is serialized like every other companion job, so it never runs Codex concurrently. That is what avoids the 403/429 websocket contention and wasted quota that raw parallel `codex exec` causes. | ||
| - Pass a reference image for editing with `--image ref.png`. Comma-separate several: `--image a.png,b.png`. | ||
|
|
||
| Operating rules: | ||
|
|
||
| - Default to foreground. With `--background`, the job is queued: report the job id and tell the user they can watch it with `/codex:status <id>` and fetch it with `/codex:result <id>`. | ||
| - `--out` refuses to overwrite an existing file. If the user wants to replace it, add `--force`. | ||
| - Return the companion stdout to the user. Do not paraphrase the `Saved image:` path. | ||
| - If the companion reports that Codex is missing or unauthenticated, tell the user to run `/codex:setup`. | ||
| - If the user gave no prompt, ask what the image should be. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| #!/usr/bin/env node | ||
| // PreToolUse(Bash) — route ALL image generation through the managed /codex:imagegen path. | ||
| // | ||
| // Image generation must go through codex-companion's serialized app-server path. | ||
| // Every other route — garden gpt-image-2 generate.js / edit.js, baoyu-image-gen, | ||
| // image2_asset.py, a bare `codex exec ... imagegen`, or a direct POST to /v1/images | ||
| // — bypasses the single serialized connection, which is the websocket contention | ||
| // (403/429) and quota burn the managed path exists to prevent. | ||
| // | ||
| // This hook prefers REWRITE: when it can map a foreign command's prompt / output / | ||
| // reference flags onto the companion's `imagegen` subcommand, it rewrites the command | ||
| // in place (hookSpecificOutput.updatedInput) so it transparently runs through the | ||
| // managed path — no extra agent round-trip. When the mapping is not safe (prompt only | ||
| // in a file, free-text `codex exec`, raw curl), it falls back to BLOCK + redirect and | ||
| // lets the agent re-issue. It never rewrites into a guess. | ||
| // | ||
| // Coding `codex exec` (rescue / executor) and the companion's own imagegen are left | ||
| // untouched. | ||
|
|
||
| import { readFileSync } from "node:fs"; | ||
|
|
||
| let raw = ""; | ||
| try { | ||
| raw = readFileSync(0, "utf8"); | ||
| } catch { | ||
| process.exit(0); | ||
| } | ||
|
|
||
| let cmd = ""; | ||
| try { | ||
| cmd = (JSON.parse(raw).tool_input?.command ?? "").toString(); | ||
| } catch { | ||
| process.exit(0); | ||
| } | ||
| if (!cmd.trim()) process.exit(0); | ||
|
|
||
| // Already the managed path — let it run. | ||
| if (/codex-companion\.mjs[\s\S]*\bimagegen\b/.test(cmd)) process.exit(0); | ||
|
|
||
| const COMPANION = `${process.env.CLAUDE_PLUGIN_ROOT ?? ""}/scripts/codex-companion.mjs`; | ||
|
|
||
| // Foreign image-gen tools and how their flags map onto the companion. | ||
| // out: flag whose value is the OUTPUT path -> companion --out | ||
| // ref: flag whose value is a REFERENCE image -> companion --image | ||
| // promptFileOnly: flags that supply the prompt via a file (can't inline) -> block | ||
| const TOOLS = [ | ||
| { re: /gpt-image-2[/\\][^\s]*\bgenerate\.js/i, prompt: ["--prompt"], out: ["--image"], ref: [], promptFileOnly: ["--promptfile"] }, | ||
| { re: /gpt-image-2[/\\][^\s]*\bedit\.js/i, prompt: ["--prompt"], out: [], ref: ["--image"], promptFileOnly: ["--promptfile"] }, | ||
| { re: /baoyu-image-gen[/\\][^\s]*main\.ts/i, prompt: ["--prompt"], out: ["--image"], ref: ["--ref", "--reference"], promptFileOnly: ["--promptfiles"] }, | ||
| { re: /image2_asset\.py/i, prompt: ["--prompt"], out: ["--output"], ref: ["--image"], promptFileOnly: [] }, | ||
| ]; | ||
|
|
||
| const tool = TOOLS.find((t) => t.re.test(cmd)); | ||
| if (tool) { | ||
| const prompt = flag(cmd, tool.prompt); | ||
| if (!prompt) block(promptOnlyInFile(cmd, tool) ? "a file-only prompt that cannot be inlined" : "an image-gen command with no inlinable --prompt"); | ||
| const out = flag(cmd, tool.out); | ||
| const ref = flag(cmd, tool.ref); | ||
| const passBg = /(^|\s)--background(\s|$)/.test(cmd) ? " --background" : ""; | ||
| const parts = [`node ${shq(COMPANION)} imagegen`]; | ||
| if (out) parts.push(`--out ${shq(out)}`); | ||
| if (ref) parts.push(`--image ${shq(ref)}`); | ||
| const rewritten = parts.join(" ") + passBg + ` ${shq(prompt)}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a foreign image command is run with required shell context, such as Useful? React with 👍 / 👎. |
||
| process.stdout.write(JSON.stringify({ | ||
| hookSpecificOutput: { | ||
| hookEventName: "PreToolUse", | ||
| permissionDecision: "allow", | ||
| permissionDecisionReason: "Rerouted through the managed /codex:imagegen path.", | ||
| updatedInput: { command: rewritten }, | ||
| }, | ||
| })); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| // Routes we cannot safely rewrite -> block + redirect. | ||
| const stripped = cmd.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, ""); | ||
| // Use STRONG image-gen intent phrases only. Bare "imagegen" / "gpt-image" collide | ||
| // with file and test names (e.g. `codex exec "fix the imagegen-route-hook test"`), | ||
| // so they must NOT trip the block — only an explicit generate-an-image instruction does. | ||
| if (/codex\s+exec/.test(stripped) && | ||
| /\bimage generation\b|\bgenerate (?:the |an |a )?image\b|built[- ]in image|generated_images/i.test(cmd)) { | ||
| block("a bare `codex exec` image generation"); | ||
| } | ||
| if (/\/v1\/images\/(generations|edits)/i.test(cmd)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a coding or debugging command merely mentions the endpoint, such as Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because the new PreToolUse hook runs on every Bash command, this substring check blocks harmless coding commands that merely contain the endpoint text, such as Useful? React with 👍 / 👎. |
||
| block("a direct POST to /v1/images (OpenAI-compatible image API)"); | ||
| } | ||
|
|
||
| process.exit(0); | ||
|
|
||
| // ── helpers ────────────────────────────────────────────────────────────────── | ||
| function flag(command, names) { | ||
| for (const n of names) { | ||
| const re = new RegExp(`${n.replace(/[-]/g, "\\$&")}(?:\\s+|=)(?:"([^"]*)"|'([^']*)'|(\\S+))`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the prompt itself contains a flag-like phrase for the same tool, for example Useful? React with 👍 / 👎. |
||
| const m = command.match(re); | ||
| if (m) return m[1] ?? m[2] ?? m[3]; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| function promptOnlyInFile(command, tool) { | ||
| return tool.promptFileOnly.some((f) => flag(command, [f]) !== null); | ||
| } | ||
|
|
||
| function shq(s) { | ||
| return `'${String(s).replace(/'/g, `'\\''`)}'`; | ||
| } | ||
|
|
||
| function block(why) { | ||
| process.stderr.write( | ||
| `BLOCKED — image generation must go through the managed /codex:imagegen path. | ||
|
|
||
| Detected: ${why}. | ||
| This route can't be auto-rewritten safely, so it's blocked rather than guessed. | ||
|
|
||
| Do instead: | ||
| - Generate via the managed path: | ||
| /codex:imagegen --out <path> <what the image should be> | ||
| (edit with --image ref.png; queue with --background) | ||
| - To recover an image a prior run already produced: | ||
| ls -t ~/.codex/generated_images/*/*.png | head | ||
| `); | ||
| process.exit(2); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing only this plugin manifest to 1.0.5 leaves the repository's release metadata inconsistent:
package.json,package-lock.json, and.claude-plugin/marketplace.jsonare still 1.0.4, sonpm run check-versionnow fails withplugins/codex/.claude-plugin/plugin.json version: expected 1.0.4, found 1.0.5. Either bump all manifests with the existing version script or leave this value aligned until release.Useful? React with 👍 / 👎.