Last active
July 27, 2026 10:38
-
-
Save heguro/ed44c269c90b2601a0626aecd9740239 to your computer and use it in GitHub Desktop.
codex-ask wrapper for codex exec JSON output
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| import { spawn } from "node:child_process"; | |
| const usage = `Usage: | |
| claude-ask [options] < prompt | |
| Options: | |
| --model <model> Model to pass to Claude Code. Default: sonnet. | |
| Aliases include haiku, sonnet, opus and fable. | |
| A full model ID is also accepted. | |
| --effort <effort> Effort level. Default: medium. | |
| Valid values: | |
| low, medium, high, xhigh, max, or ultracode. | |
| ultracode: xhigh with automatic workflow | |
| orchestration. | |
| --fallback-model <model> Model to use if the primary model is unavailable. | |
| --max-turns <count> Stop after this many agentic turns. | |
| --max-budget-usd <usd> Stop when the API spend reaches this amount. | |
| --resume <session> Resume an existing Claude Code session by ID or name. | |
| --ephemeral Do not persist the session; it cannot be resumed. | |
| --safe Respect normal Claude Code permission rules instead | |
| of bypassing permission checks. | |
| --json Print a normalized JSON object instead of plain text. | |
| --exit-code In JSON mode, exit non-zero when Claude Code fails. | |
| Plain text mode always exits non-zero on failure. | |
| -h, --help Show this help. | |
| Output: | |
| By default, stdout is the final Claude response text. For persisted sessions, | |
| stderr also includes: | |
| thread_id=... | |
| With --json, stdout is a single normalized JSON object: | |
| {"ok":true,"thread_id":"...","text":"...","error":null,"exit_code":0} | |
| `; | |
| const argv = process.argv.slice(2); | |
| const jsonMode = argv.includes("--json"); | |
| const useExitCode = argv.includes("--exit-code"); | |
| function jsonResult(ok, sessionId, text, error, exitCode) { | |
| return { | |
| ok, | |
| thread_id: ephemeral ? null : sessionId, | |
| text: ok ? text : null, | |
| error: ok ? null : error, | |
| exit_code: exitCode, | |
| }; | |
| } | |
| function writeJson(result, processExitCode = 0) { | |
| process.stdout.write(`${JSON.stringify(result)}\n`); | |
| process.exit(processExitCode); | |
| } | |
| function failBeforeSpawn(error) { | |
| const exitCode = 2; | |
| if (jsonMode) { | |
| writeJson( | |
| jsonResult(false, null, null, error, exitCode), | |
| useExitCode ? exitCode : 0, | |
| ); | |
| } | |
| process.stderr.write(`claude-ask: error: ${error}\n`); | |
| process.exit(exitCode); | |
| } | |
| function optionValue(args, index, option) { | |
| const value = args[index + 1]; | |
| if (!value || value.startsWith("--")) { | |
| failBeforeSpawn(`Missing value for ${option}.`); | |
| } | |
| return value; | |
| } | |
| function positiveNumber(value, option, { integer = false } = {}) { | |
| const number = Number(value); | |
| if (!Number.isFinite(number) || number <= 0 || (integer && !Number.isInteger(number))) { | |
| failBeforeSpawn(`${option} must be a positive ${integer ? "integer" : "number"}.`); | |
| } | |
| } | |
| let model = "sonnet"; | |
| let effort = "medium"; | |
| let fallbackModel; | |
| let maxTurns; | |
| let maxBudgetUsd; | |
| let resume; | |
| let ephemeral = false; | |
| let safe = false; | |
| for (let i = 0; i < argv.length; i++) { | |
| const arg = argv[i]; | |
| if (arg === "-h" || arg === "--help") { | |
| process.stdout.write(usage); | |
| process.exit(0); | |
| } else if (arg === "--json" || arg === "--exit-code") { | |
| // These are read before parsing so argument errors are normalized | |
| // regardless of option order. | |
| } else if (arg === "--ephemeral") { | |
| ephemeral = true; | |
| } else if (arg === "--safe") { | |
| safe = true; | |
| } else if ( | |
| arg === "--model" || | |
| arg === "--effort" || | |
| arg === "--fallback-model" || | |
| arg === "--max-turns" || | |
| arg === "--max-budget-usd" || | |
| arg === "--resume" | |
| ) { | |
| const value = optionValue(argv, i, arg); | |
| i++; | |
| if (arg === "--model") model = value; | |
| else if (arg === "--effort") effort = value; | |
| else if (arg === "--fallback-model") fallbackModel = value; | |
| else if (arg === "--max-turns") maxTurns = value; | |
| else if (arg === "--max-budget-usd") maxBudgetUsd = value; | |
| else resume = value; | |
| } else { | |
| failBeforeSpawn(`Unknown option: ${arg}.`); | |
| } | |
| } | |
| if (!["low", "medium", "high", "xhigh", "max"].includes(effort)) { | |
| failBeforeSpawn( | |
| `Invalid effort: ${effort}. Use low, medium, high, xhigh, or max.`, | |
| ); | |
| } | |
| if (maxTurns !== undefined) { | |
| positiveNumber(maxTurns, "--max-turns", { integer: true }); | |
| } | |
| if (maxBudgetUsd !== undefined) { | |
| positiveNumber(maxBudgetUsd, "--max-budget-usd"); | |
| } | |
| if (resume && ephemeral) { | |
| failBeforeSpawn("--ephemeral cannot be used with --resume."); | |
| } | |
| const claudeArgs = [ | |
| "-p", | |
| "--output-format", | |
| "stream-json", | |
| "--verbose", | |
| "--model", | |
| model, | |
| "--effort", | |
| effort, | |
| ]; | |
| if (fallbackModel) claudeArgs.push("--fallback-model", fallbackModel); | |
| if (maxTurns) claudeArgs.push("--max-turns", maxTurns); | |
| if (maxBudgetUsd) claudeArgs.push("--max-budget-usd", maxBudgetUsd); | |
| if (resume) claudeArgs.push("--resume", resume); | |
| if (ephemeral) claudeArgs.push("--no-session-persistence"); | |
| if (!safe) claudeArgs.push("--dangerously-skip-permissions"); | |
| // With no positional prompt, Claude Code reads the prompt from stdin. | |
| const child = spawn("claude", claudeArgs, { | |
| stdio: ["pipe", "pipe", "pipe"], | |
| }); | |
| process.stdin.pipe(child.stdin); | |
| let stdoutBuffer = ""; | |
| let stderr = ""; | |
| let spawnError = null; | |
| let sessionId = null; | |
| let threadIdPrinted = false; | |
| let result = null; | |
| let malformedJson = false; | |
| function maybePrintThreadId() { | |
| if (!ephemeral && sessionId && !threadIdPrinted) { | |
| process.stderr.write(`thread_id=${sessionId}\n`); | |
| threadIdPrinted = true; | |
| } | |
| } | |
| function handleEvent(line) { | |
| if (!line.trim()) return; | |
| let event; | |
| try { | |
| event = JSON.parse(line); | |
| } catch { | |
| malformedJson = true; | |
| return; | |
| } | |
| if (!sessionId && typeof event.session_id === "string") { | |
| sessionId = event.session_id; | |
| maybePrintThreadId(); | |
| } | |
| if (event.type === "result") { | |
| result = event; | |
| } | |
| } | |
| child.stdout.on("data", (chunk) => { | |
| stdoutBuffer += chunk; | |
| for (let newline; (newline = stdoutBuffer.indexOf("\n")) >= 0;) { | |
| const line = stdoutBuffer.slice(0, newline); | |
| stdoutBuffer = stdoutBuffer.slice(newline + 1); | |
| handleEvent(line); | |
| } | |
| }); | |
| child.stderr.on("data", (chunk) => { | |
| stderr += chunk; | |
| }); | |
| child.on("error", (error) => { | |
| spawnError = error; | |
| }); | |
| child.stdin.on("error", (error) => { | |
| // A startup failure can close Claude's stdin while the prompt is still | |
| // being piped. The process result below carries the useful error. | |
| if (error.code !== "EPIPE") spawnError ??= error; | |
| }); | |
| child.on("close", (code) => { | |
| const exitCode = code ?? 1; | |
| if (spawnError) { | |
| const spawnExitCode = 1; | |
| if (jsonMode) { | |
| writeJson( | |
| jsonResult(false, null, null, spawnError.message, spawnExitCode), | |
| useExitCode ? spawnExitCode : 0, | |
| ); | |
| } | |
| process.stderr.write(`claude-ask: error: ${spawnError.message}\n`); | |
| process.exit(spawnExitCode); | |
| } | |
| handleEvent(stdoutBuffer); | |
| if (!sessionId && typeof result?.session_id === "string") { | |
| sessionId = result.session_id; | |
| maybePrintThreadId(); | |
| } | |
| const text = typeof result?.result === "string" ? result.result : null; | |
| const ok = | |
| exitCode === 0 && | |
| result?.type === "result" && | |
| result?.subtype === "success" && | |
| result?.is_error === false && | |
| text !== null; | |
| const stderrLine = | |
| stderr.trim().split("\n").filter(Boolean).at(-1) ?? null; | |
| const reason = | |
| (!ok && text) || | |
| result?.error?.message || | |
| stderrLine || | |
| (result === null | |
| ? malformedJson | |
| ? "Claude Code returned invalid stream-json output." | |
| : "Claude Code did not return a final result event." | |
| : "Claude Code failed without an error message."); | |
| const resultExitCode = ok ? 0 : (exitCode || 1); | |
| if (jsonMode) { | |
| writeJson( | |
| jsonResult(ok, sessionId, text, reason, resultExitCode), | |
| ok || !useExitCode ? 0 : resultExitCode, | |
| ); | |
| } | |
| if (ok) { | |
| process.stdout.write(text); | |
| if (!text.endsWith("\n")) process.stdout.write("\n"); | |
| process.exit(0); | |
| } | |
| process.stderr.write(`claude-ask: error: ${reason}\n`); | |
| process.exit(exitCode || 1); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| import { spawn } from "node:child_process"; | |
| const usage = `Usage: | |
| codex-ask [options] < prompt | |
| Options: | |
| --model <model> Model to pass to codex exec. Default: gpt-5.6-sol. | |
| Pass the model name, e.g.: | |
| gpt-5.6-sol (roughly Opus-class) | |
| gpt-5.6-terra (roughly Sonnet-class) | |
| gpt-5.6-luna (roughly upper/larger Haiku-class) | |
| gpt-5.4-mini (roughly Haiku-class) | |
| gpt-5.5 (older model; roughly Opus-class) | |
| gpt-5.4 (older model; roughly Sonnet-class) | |
| --effort <effort> Reasoning effort. Default: medium. | |
| Valid values: low, medium, high, or xhigh. | |
| --resume <thread> Resume an existing Codex thread. | |
| --ephemeral Run without persisting the Codex session to disk; it | |
| cannot be resumed later. | |
| --json Print a single JSON object instead of plain text. | |
| --exit-code In JSON mode, exit non-zero when Codex fails. Plain text | |
| mode always exits non-zero on failure. | |
| -h, --help Show this help. | |
| Output: | |
| By default, stdout is the final Codex response text. For resumable sessions, | |
| stderr includes this as soon as Codex reports it: | |
| thread_id=... | |
| With --json, stdout is a single JSON object: | |
| {"ok":true,"thread_id":"...","text":"...","error":null,"exit_code":0} | |
| `; | |
| function jsonResult(ok, threadId, text, error, exitCode) { | |
| return { | |
| ok, | |
| thread_id: ephemeral ? null : threadId, | |
| text: ok ? text : null, | |
| error: ok ? null : error, | |
| exit_code: exitCode, | |
| }; | |
| } | |
| function writeResult(result, processExitCode = 0) { | |
| process.stdout.write(JSON.stringify(result) + "\n"); | |
| process.exit(processExitCode); | |
| } | |
| function failBeforeSpawn(error, processExitCode = 0) { | |
| if (jsonMode) { | |
| writeResult(jsonResult(false, null, null, error, 1), processExitCode); | |
| } | |
| process.stderr.write(`codex-ask: error: ${error}\n`); | |
| process.exit(processExitCode || 1); | |
| } | |
| const a = process.argv.slice(2); | |
| let model = "gpt-5.6-sol", effort = "medium", resume, useExitCode = false, ephemeral = false, jsonMode = false; | |
| for (let i = 0; i < a.length; i++) { | |
| const arg = a[i]; | |
| if (arg === "-h" || arg === "--help") { | |
| process.stdout.write(usage); | |
| process.exit(0); | |
| } else if (arg === "--exit-code") { | |
| useExitCode = true; | |
| } else if (arg === "--ephemeral") { | |
| ephemeral = true; | |
| } else if (arg === "--json") { | |
| jsonMode = true; | |
| } else if (arg === "--model" || arg === "--effort" || arg === "--resume") { | |
| const value = a[++i]; | |
| if (!value || value.startsWith("--")) { | |
| failBeforeSpawn(`Missing value for ${arg}.`, useExitCode ? 2 : 0); | |
| } | |
| if (arg === "--model") model = value; | |
| else if (arg === "--effort") effort = value; | |
| else resume = value; | |
| } else { | |
| failBeforeSpawn(`Unknown option: ${arg}.`, useExitCode ? 2 : 0); | |
| } | |
| } | |
| if (resume && ephemeral) { | |
| failBeforeSpawn("--ephemeral cannot be used with --resume.", useExitCode ? 2 : 0); | |
| } | |
| const args = resume | |
| ? ["exec", "resume", resume, "--json", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"] | |
| : ["exec", "--json", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"]; | |
| if (ephemeral) args.push("--ephemeral"); | |
| if (model) args.push("--model", model); | |
| if (effort) args.push("-c", `model_reasoning_effort=${effort}`); | |
| args.push("-"); | |
| // stderr は inherit せず capture する(起動失敗はここにしか出ない) | |
| const child = spawn("codex", args, { stdio: ["pipe", "pipe", "pipe"] }); | |
| process.stdin.pipe(child.stdin); | |
| let buf = "", threadId = null, threadIdPrinted = false, lastText = null; | |
| let turnFailed = null, topError = null, stderr = ""; | |
| let spawnError = null; | |
| child.on("error", (err) => { | |
| spawnError = err; | |
| }); | |
| child.stderr.on("data", (d) => { stderr += d; }); | |
| function maybePrintThreadId() { | |
| if (!ephemeral && threadId && !threadIdPrinted) { | |
| process.stderr.write(`thread_id=${threadId}\n`); | |
| threadIdPrinted = true; | |
| } | |
| } | |
| child.stdout.on("data", (d) => { | |
| buf += d; | |
| for (let nl; (nl = buf.indexOf("\n")) >= 0; ) { | |
| const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); | |
| if (!line) continue; | |
| let ev; try { ev = JSON.parse(line); } catch { continue; } | |
| switch (ev.type) { | |
| case "thread.started": | |
| threadId = ev.thread_id; | |
| maybePrintThreadId(); | |
| break; | |
| case "item.completed": | |
| if (ev.item?.type === "agent_message") lastText = ev.item.text; | |
| // 注意: ev.item.type === "error" は warning/deprecation なので失敗扱いしない | |
| break; | |
| case "turn.failed": turnFailed = ev.error?.message ?? "turn failed"; break; | |
| case "error": topError = ev.message; break; // top-level のみ失敗寄り | |
| } | |
| } | |
| }); | |
| child.on("close", (code) => { | |
| if (spawnError) { | |
| if (jsonMode) { | |
| writeResult(jsonResult(false, null, null, spawnError.message, 1), useExitCode ? 1 : 0); | |
| } | |
| process.stderr.write(`codex-ask: error: ${spawnError.message}\n`); | |
| process.exit(1); | |
| } | |
| const ok = code === 0 && lastText != null; | |
| const reason = turnFailed ?? topError ?? (stderr.trim().split("\n").pop() || null); | |
| if (jsonMode) { | |
| writeResult(jsonResult(ok, threadId, lastText, reason, code), ok || !useExitCode ? 0 : (code || 1)); | |
| } | |
| if (ok) { | |
| maybePrintThreadId(); | |
| process.stdout.write(lastText); | |
| if (!lastText.endsWith("\n")) process.stdout.write("\n"); | |
| process.exit(0); | |
| } | |
| process.stderr.write(`codex-ask: error: ${reason ?? "Codex failed without an error message."}\n`); | |
| process.exit(code || 1); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| import { spawn } from "node:child_process"; | |
| const usage = `Usage: | |
| cursor-ask [options] < prompt | |
| Options: | |
| --model <model> Model to pass to agent. Default: composer-2.5-fast. | |
| Examples: | |
| cursor-grok-4.5-{low,medium,high}{,-fast} | |
| composer-2.5{,-fast} | |
| auto | |
| --mode <mode> Agent mode: ask or plan. Default: normal Agent mode. | |
| --resume <session> Resume an existing Cursor session. | |
| --json Print a normalized JSON object instead of plain text. | |
| --exit-code In JSON mode, exit non-zero when Agent fails. Plain text | |
| mode always exits non-zero on failure. | |
| --ephemeral Unsupported: Cursor CLI has no ephemeral-session flag. | |
| -h, --help Show this help. | |
| Output: | |
| By default, stdout is the final Agent response text. As soon as Cursor | |
| reports a session ID, stderr includes: | |
| thread_id=... | |
| With --json, stdout is a single JSON object: | |
| {"ok":true,"thread_id":"...","text":"...","error":null,"exit_code":0} | |
| `; | |
| function jsonResult(ok, sessionId, text, error, exitCode) { | |
| return { | |
| ok, | |
| thread_id: sessionId, | |
| text: ok ? text : null, | |
| error: ok ? null : error, | |
| exit_code: exitCode, | |
| }; | |
| } | |
| function writeResult(result, processExitCode = 0) { | |
| process.stdout.write(JSON.stringify(result) + "\n"); | |
| process.exit(processExitCode); | |
| } | |
| let jsonMode = false; | |
| let useExitCode = false; | |
| let safeMode = false; | |
| function failBeforeSpawn(error, processExitCode = 0) { | |
| if (jsonMode) { | |
| writeResult(jsonResult(false, null, null, error, 1), processExitCode); | |
| } | |
| process.stderr.write(`cursor-ask: error: ${error}\n`); | |
| process.exit(processExitCode || 1); | |
| } | |
| const a = process.argv.slice(2); | |
| let model = "composer-2.5-fast"; | |
| let mode = null; | |
| let resume; | |
| for (let i = 0; i < a.length; i++) { | |
| const arg = a[i]; | |
| if (arg === "-h" || arg === "--help") { | |
| process.stdout.write(usage); | |
| process.exit(0); | |
| } else if (arg === "--json") { | |
| jsonMode = true; | |
| } else if (arg === "--exit-code") { | |
| useExitCode = true; | |
| } else if (arg === "--safe") { | |
| safeMode = true; | |
| } else if (arg === "--ephemeral") { | |
| failBeforeSpawn("--ephemeral is not supported by Cursor CLI.", useExitCode ? 2 : 0); | |
| } else if (arg === "--model" || arg === "--mode" || arg === "--resume") { | |
| const value = a[++i]; | |
| if (!value || value.startsWith("--")) { | |
| failBeforeSpawn(`Missing value for ${arg}.`, useExitCode ? 2 : 0); | |
| } | |
| if (arg === "--model") model = value; | |
| else if (arg === "--mode") mode = value; | |
| else resume = value; | |
| } else { | |
| failBeforeSpawn(`Unknown option: ${arg}.`, useExitCode ? 2 : 0); | |
| } | |
| } | |
| if (mode && !["ask", "plan"].includes(mode)) { | |
| failBeforeSpawn(`Invalid mode: ${mode}. Use ask or plan.`, useExitCode ? 2 : 0); | |
| } | |
| const args = [ | |
| "--trust", | |
| "-p", | |
| "--output-format", | |
| "stream-json", | |
| "--model", | |
| model, | |
| ]; | |
| if (mode) args.push("--mode", mode); | |
| if (!safeMode) args.push("--force", "--sandbox", "disabled"); | |
| if (resume) args.push("--resume", resume); | |
| // Passing no positional prompt makes Cursor read the prompt from stdin. | |
| const child = spawn("agent", args, { stdio: ["pipe", "pipe", "pipe"] }); | |
| process.stdin.pipe(child.stdin); | |
| let stdoutBuffer = ""; | |
| let stderr = ""; | |
| let spawnError = null; | |
| let sessionId = null; | |
| let threadIdPrinted = false; | |
| let result = null; | |
| let malformedJson = false; | |
| function maybePrintThreadId() { | |
| if (sessionId && !threadIdPrinted) { | |
| process.stderr.write(`thread_id=${sessionId}\n`); | |
| threadIdPrinted = true; | |
| } | |
| } | |
| function handleEvent(line) { | |
| if (!line.trim()) return; | |
| let event; | |
| try { | |
| event = JSON.parse(line); | |
| } catch { | |
| malformedJson = true; | |
| return; | |
| } | |
| if (!sessionId && typeof event.session_id === "string") { | |
| sessionId = event.session_id; | |
| maybePrintThreadId(); | |
| } | |
| if (event.type === "result") { | |
| result = event; | |
| } | |
| } | |
| child.stdout.on("data", (d) => { | |
| stdoutBuffer += d; | |
| for (let newline; (newline = stdoutBuffer.indexOf("\n")) >= 0;) { | |
| const line = stdoutBuffer.slice(0, newline); | |
| stdoutBuffer = stdoutBuffer.slice(newline + 1); | |
| handleEvent(line); | |
| } | |
| }); | |
| child.stderr.on("data", (d) => { stderr += d; }); | |
| child.on("error", (err) => { spawnError = err; }); | |
| child.stdin.on("error", (err) => { | |
| if (err.code !== "EPIPE") spawnError ??= err; | |
| }); | |
| child.on("close", (code) => { | |
| const exitCode = code ?? 1; | |
| if (spawnError) { | |
| const spawnExitCode = 1; | |
| if (jsonMode) { | |
| writeResult( | |
| jsonResult(false, null, null, spawnError.message, spawnExitCode), | |
| useExitCode ? spawnExitCode : 0, | |
| ); | |
| } | |
| process.stderr.write(`cursor-ask: error: ${spawnError.message}\n`); | |
| process.exit(spawnExitCode); | |
| } | |
| handleEvent(stdoutBuffer); | |
| if (!sessionId && typeof result?.session_id === "string") { | |
| sessionId = result.session_id; | |
| maybePrintThreadId(); | |
| } | |
| const text = typeof result?.result === "string" ? result.result : null; | |
| const ok = exitCode === 0 && result?.type === "result" && | |
| result?.subtype === "success" && result?.is_error === false && text != null; | |
| const stderrLine = | |
| stderr.trim().split("\n").filter(Boolean).pop() ?? null; | |
| const reason = | |
| (!ok && text) || | |
| result?.error?.message || | |
| stderrLine || | |
| (result === null && malformedJson | |
| ? "Cursor Agent returned invalid stream-json output." | |
| : "Cursor Agent failed without an error message."); | |
| const resultExitCode = ok ? 0 : (exitCode || 1); | |
| if (jsonMode) { | |
| writeResult( | |
| jsonResult(ok, sessionId, text, reason, resultExitCode), | |
| ok || !useExitCode ? 0 : resultExitCode, | |
| ); | |
| } | |
| if (ok) { | |
| process.stdout.write(text); | |
| if (!text.endsWith("\n")) process.stdout.write("\n"); | |
| process.exit(0); | |
| } | |
| process.stderr.write(`cursor-ask: error: ${reason}\n`); | |
| process.exit(resultExitCode); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment