Skip to content

Instantly share code, notes, and snippets.

@heguro
Last active July 27, 2026 10:38
Show Gist options
  • Select an option

  • Save heguro/ed44c269c90b2601a0626aecd9740239 to your computer and use it in GitHub Desktop.

Select an option

Save heguro/ed44c269c90b2601a0626aecd9740239 to your computer and use it in GitHub Desktop.
codex-ask wrapper for codex exec JSON output
#!/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);
});
#!/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);
});
#!/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