From 05e32203ee624486d831d3d749e09ac52054dbd5 Mon Sep 17 00:00:00 2001 From: "swe-agent[bot]" <0+swe-agent[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:47:04 +0800 Subject: [PATCH] optimize codex skills --- skills/codex/{SKILLS.md => SKILL.md} | 35 ++++-- skills/codex/scripts/codex.js | 178 +++++++++++++++++++++++++++ skills/codex/scripts/codex.sh | 15 --- 3 files changed, 206 insertions(+), 22 deletions(-) rename skills/codex/{SKILLS.md => SKILL.md} (55%) create mode 100755 skills/codex/scripts/codex.js delete mode 100755 skills/codex/scripts/codex.sh diff --git a/skills/codex/SKILLS.md b/skills/codex/SKILL.md similarity index 55% rename from skills/codex/SKILLS.md rename to skills/codex/SKILL.md index 865e4e2..1a12151 100644 --- a/skills/codex/SKILLS.md +++ b/skills/codex/SKILL.md @@ -18,11 +18,18 @@ Execute Codex CLI commands and parse structured JSON responses. Supports file re ## Usage +通过 Bash tool 调用: ```bash -bash scripts/codex.sh "" [model] [working_dir] +node ~/.claude/skills/codex/scripts/codex.js "" [model] [working_dir] ``` -**Timeout**: Set `timeout: 7200000` (2 hours) in Bash tool for long tasks. +## Timeout Control + +- **Built-in**: Script enforces 2-hour timeout by default +- **Override**: Set `CODEX_TIMEOUT` environment variable (in milliseconds, e.g., `CODEX_TIMEOUT=3600000` for 1 hour) +- **Behavior**: On timeout, sends SIGTERM, then SIGKILL after 5s if process doesn't exit +- **Exit code**: Returns 124 on timeout (consistent with GNU timeout) +- **Bash tool**: Always set `timeout: 7200000` parameter for double protection ### Parameters @@ -47,26 +54,41 @@ Error format: ERROR: Error message ``` +### Invocation Pattern + +When calling via Bash tool, always include the timeout parameter: +``` +Bash tool parameters: +- command: node ~/.claude/skills/codex/scripts/codex.js "" [model] [working_dir] +- timeout: 7200000 +- description: +``` + ### Examples **Basic code analysis:** ```bash -bash scripts/codex.sh "explain @src/main.ts" +# Via Bash tool with timeout parameter +node ~/.claude/skills/codex/scripts/codex.js "explain @src/main.ts" +# timeout: 7200000 ``` **Refactoring with specific model:** ```bash -bash scripts/codex.sh "refactor @src/utils for performance" "gpt-5" +node ~/.claude/skills/codex/scripts/codex.js "refactor @src/utils for performance" "gpt-5" +# timeout: 7200000 ``` **Multi-file analysis:** ```bash -bash scripts/codex.sh "analyze @. and find security issues" "gpt-5-codex" "/path/to/project" +node ~/.claude/skills/codex/scripts/codex.js "analyze @. and find security issues" "gpt-5-codex" "/path/to/project" +# timeout: 7200000 ``` **Quick task:** ```bash -bash scripts/codex.sh "add comments to @utils.js" "gpt-5-codex" +node ~/.claude/skills/codex/scripts/codex.js "add comments to @utils.js" "gpt-5-codex" +# timeout: 7200000 ``` ## Notes @@ -75,4 +97,3 @@ bash scripts/codex.sh "add comments to @utils.js" "gpt-5-codex" - Uses `--skip-git-repo-check` to work in any directory - Streams progress, returns only final agent message - Requires Codex CLI installed and authenticated -- Use `timeout: 7200000` (2 hours) for complex tasks that may take longer diff --git a/skills/codex/scripts/codex.js b/skills/codex/scripts/codex.js new file mode 100755 index 0000000..fb0f23d --- /dev/null +++ b/skills/codex/scripts/codex.js @@ -0,0 +1,178 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; + +const DEFAULT_MODEL = 'gpt-5-codex'; +const DEFAULT_WORKDIR = '.'; +const DEFAULT_TIMEOUT_MS = 7_200_000; // 2 hours +const FORCE_KILL_DELAY_MS = 5_000; + +const args = process.argv.slice(2); +const [task, modelArg, workdirArg] = args; + +const logError = (message) => { + process.stderr.write(`ERROR: ${message}\n`); +}; + +const logWarn = (message) => { + process.stderr.write(`WARN: ${message}\n`); +}; + +if (!task) { + logError('Task required'); + process.exit(1); +} + +const model = modelArg || DEFAULT_MODEL; +const workdir = workdirArg || DEFAULT_WORKDIR; + +const resolveTimeout = () => { + const raw = process.env.CODEX_TIMEOUT; + if (raw == null || raw === '') { + return DEFAULT_TIMEOUT_MS; + } + + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) { + logWarn(`Invalid CODEX_TIMEOUT "${raw}", falling back to ${DEFAULT_TIMEOUT_MS}ms`); + return DEFAULT_TIMEOUT_MS; + } + + return parsed; +}; + +const codexArgs = [ + 'e', + '-m', + model, + '--dangerously-bypass-approvals-and-sandbox', + '--skip-git-repo-check', + '-C', + workdir, + '--json', + task, +]; + +const child = spawn('codex', codexArgs, { + stdio: ['ignore', 'pipe', 'inherit'], +}); + +let timedOut = false; +let lastAgentMessage = null; +let stdoutBuffer = ''; +let forceKillTimer = null; + +const timeoutMs = resolveTimeout(); + +const forceTerminate = () => { + if (!child.killed) { + child.kill('SIGTERM'); + forceKillTimer = setTimeout(() => { + if (!child.killed) { + child.kill('SIGKILL'); + } + }, FORCE_KILL_DELAY_MS); + } +}; + +const timeoutHandle = setTimeout(() => { + timedOut = true; + logError('Codex execution timeout'); + forceTerminate(); +}, timeoutMs); + +const normalizeText = (text) => { + if (typeof text === 'string') { + return text; + } + if (Array.isArray(text)) { + return text.join(''); + } + return null; +}; + +const handleJsonLine = (line) => { + const trimmed = line.trim(); + if (!trimmed) { + return; + } + + let event; + try { + event = JSON.parse(trimmed); + } catch (err) { + logWarn(`Failed to parse Codex output line: ${trimmed}`); + return; + } + + if ( + event && + event.type === 'item.completed' && + event.item && + event.item.type === 'agent_message' + ) { + const text = normalizeText(event.item.text); + if (text != null) { + lastAgentMessage = text; + } + } +}; + +child.stdout.on('data', (chunk) => { + stdoutBuffer += chunk.toString('utf8'); + let newlineIndex = stdoutBuffer.indexOf('\n'); + + while (newlineIndex !== -1) { + const line = stdoutBuffer.slice(0, newlineIndex); + stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1); + handleJsonLine(line); + newlineIndex = stdoutBuffer.indexOf('\n'); + } +}); + +child.stdout.on('end', () => { + if (stdoutBuffer) { + handleJsonLine(stdoutBuffer); + stdoutBuffer = ''; + } +}); + +child.on('error', (err) => { + clearTimeout(timeoutHandle); + if (forceKillTimer) { + clearTimeout(forceKillTimer); + } + logError(`Failed to start Codex CLI: ${err.message}`); + process.exit(1); +}); + +child.on('close', (code, signal) => { + clearTimeout(timeoutHandle); + if (forceKillTimer) { + clearTimeout(forceKillTimer); + } + + if (timedOut) { + process.exit(124); + return; + } + + if (code === 0) { + if (lastAgentMessage != null) { + process.stdout.write(`${lastAgentMessage}\n`); + process.exit(0); + } else { + logError('Codex completed without an agent_message output'); + process.exit(1); + } + return; + } + + if (signal) { + logError(`Codex terminated with signal ${signal}`); + process.exit(code ?? 1); + return; + } + + logError(`Codex exited with status ${code}`); + process.exit(code ?? 1); +}); diff --git a/skills/codex/scripts/codex.sh b/skills/codex/scripts/codex.sh deleted file mode 100755 index 120792d..0000000 --- a/skills/codex/scripts/codex.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -euo pipefail - -TASK="${1:-}" -MODEL="${2:-gpt-5-codex}" -WORKDIR="${3:-.}" - -if [ -z "$TASK" ]; then - echo "ERROR: Task required" >&2 - exit 1 -fi - -codex e -m "$MODEL" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check -C "$WORKDIR" --json "$TASK" 2>/dev/null | \ - jq -r 'select(.type == "item.completed" and .item.type == "agent_message") | .item.text' | \ - tail -n 1