mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-11 03:23:50 +08:00
optimize codex skills
This commit is contained in:
@@ -18,9 +18,21 @@ Execute Codex CLI commands and parse structured JSON responses. Supports file re
|
||||
|
||||
## Usage
|
||||
|
||||
通过 Bash tool 调用:
|
||||
**推荐方式**(使用 uv run,自动管理 Python 环境):
|
||||
```bash
|
||||
node ~/.claude/skills/codex/scripts/codex.js "<task>" [model] [working_dir]
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
```
|
||||
|
||||
**备选方式**(直接执行或使用 Python):
|
||||
```bash
|
||||
~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
# 或
|
||||
python3 ~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
```
|
||||
|
||||
恢复会话:
|
||||
```bash
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py resume <session_id> "<task>" [model] [working_dir]
|
||||
```
|
||||
|
||||
## Timeout Control
|
||||
@@ -37,19 +49,19 @@ node ~/.claude/skills/codex/scripts/codex.js "<task>" [model] [working_dir]
|
||||
- `model` (optional): Model to use (default: gpt-5-codex)
|
||||
- `gpt-5-codex`: Default, optimized for code
|
||||
- `gpt-5`: Fast general purpose
|
||||
- `o3`: Deep reasoning
|
||||
- `o4-mini`: Quick tasks
|
||||
- `codex-1`: Software engineering
|
||||
- `working_dir` (optional): Working directory (default: current)
|
||||
|
||||
### Return Format
|
||||
|
||||
Extracts `agent_message` from Codex JSON stream:
|
||||
Extracts `agent_message` from Codex JSON stream and appends session ID:
|
||||
```
|
||||
Agent response text here...
|
||||
|
||||
---
|
||||
SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
```
|
||||
|
||||
Error format:
|
||||
Error format (stderr):
|
||||
```
|
||||
ERROR: Error message
|
||||
```
|
||||
@@ -59,41 +71,69 @@ ERROR: Error message
|
||||
When calling via Bash tool, always include the timeout parameter:
|
||||
```
|
||||
Bash tool parameters:
|
||||
- command: node ~/.claude/skills/codex/scripts/codex.js "<task>" [model] [working_dir]
|
||||
- command: uv run ~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
- timeout: 7200000
|
||||
- description: <brief description of the task>
|
||||
```
|
||||
|
||||
Alternatives:
|
||||
```
|
||||
# Direct execution (simplest)
|
||||
- command: ~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
|
||||
# Using python3
|
||||
- command: python3 ~/.claude/skills/codex/scripts/codex.py "<task>" [model] [working_dir]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Basic code analysis:**
|
||||
```bash
|
||||
# Via Bash tool with timeout parameter
|
||||
node ~/.claude/skills/codex/scripts/codex.js "explain @src/main.ts"
|
||||
# Recommended: via uv run (auto-manages Python environment)
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py "explain @src/main.ts"
|
||||
# timeout: 7200000
|
||||
|
||||
# Alternative: direct execution
|
||||
~/.claude/skills/codex/scripts/codex.py "explain @src/main.ts"
|
||||
```
|
||||
|
||||
**Refactoring with specific model:**
|
||||
```bash
|
||||
node ~/.claude/skills/codex/scripts/codex.js "refactor @src/utils for performance" "gpt-5"
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py "refactor @src/utils for performance" "gpt-5"
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Multi-file analysis:**
|
||||
```bash
|
||||
node ~/.claude/skills/codex/scripts/codex.js "analyze @. and find security issues" "gpt-5-codex" "/path/to/project"
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py "analyze @. and find security issues" "gpt-5-codex" "/path/to/project"
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Quick task:**
|
||||
**Resume previous session:**
|
||||
```bash
|
||||
node ~/.claude/skills/codex/scripts/codex.js "add comments to @utils.js" "gpt-5-codex"
|
||||
# First session
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py "add comments to @utils.js" "gpt-5-codex"
|
||||
# Output includes: SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
|
||||
# Continue the conversation
|
||||
uv run ~/.claude/skills/codex/scripts/codex.py resume 019a7247-ac9d-71f3-89e2-a823dbd8fd14 "now add type hints"
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Using python3 directly (alternative):**
|
||||
```bash
|
||||
python3 ~/.claude/skills/codex/scripts/codex.py "your task here"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Runs with `--dangerously-bypass-approvals-and-sandbox` for automation
|
||||
- **Recommended**: Use `uv run` for automatic Python environment management (requires uv installed)
|
||||
- **Alternative**: Direct execution `./codex.py` (uses system Python via shebang)
|
||||
- Python implementation using standard library (zero dependencies)
|
||||
- Cross-platform compatible (Windows/macOS/Linux)
|
||||
- PEP 723 compliant (inline script metadata)
|
||||
- Runs with `--dangerously-bypass-approvals-and-sandbox` for automation (new sessions only)
|
||||
- Uses `--skip-git-repo-check` to work in any directory
|
||||
- Streams progress, returns only final agent message
|
||||
- Every execution returns a session ID for resuming conversations
|
||||
- Requires Codex CLI installed and authenticated
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
#!/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);
|
||||
});
|
||||
198
skills/codex/scripts/codex.py
Executable file
198
skills/codex/scripts/codex.py
Executable file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.8"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Codex CLI wrapper with cross-platform support and session management.
|
||||
|
||||
Usage:
|
||||
New session: uv run codex.py "task" [model] [workdir]
|
||||
Resume: uv run codex.py resume <session_id> "task" [model] [workdir]
|
||||
Alternative: python3 codex.py "task"
|
||||
Direct exec: ./codex.py "task"
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
DEFAULT_MODEL = 'gpt-5-codex'
|
||||
DEFAULT_WORKDIR = '.'
|
||||
DEFAULT_TIMEOUT = 7200 # 2 hours in seconds
|
||||
FORCE_KILL_DELAY = 5
|
||||
|
||||
|
||||
def log_error(message: str):
|
||||
"""输出错误信息到 stderr"""
|
||||
sys.stderr.write(f"ERROR: {message}\n")
|
||||
|
||||
|
||||
def log_warn(message: str):
|
||||
"""输出警告信息到 stderr"""
|
||||
sys.stderr.write(f"WARN: {message}\n")
|
||||
|
||||
|
||||
def resolve_timeout() -> int:
|
||||
"""解析超时配置(秒)"""
|
||||
raw = os.environ.get('CODEX_TIMEOUT', '')
|
||||
if not raw:
|
||||
return DEFAULT_TIMEOUT
|
||||
|
||||
try:
|
||||
parsed = int(raw)
|
||||
if parsed <= 0:
|
||||
log_warn(f"Invalid CODEX_TIMEOUT '{raw}', falling back to {DEFAULT_TIMEOUT}s")
|
||||
return DEFAULT_TIMEOUT
|
||||
# 环境变量是毫秒,转换为秒
|
||||
return parsed // 1000 if parsed > 10000 else parsed
|
||||
except ValueError:
|
||||
log_warn(f"Invalid CODEX_TIMEOUT '{raw}', falling back to {DEFAULT_TIMEOUT}s")
|
||||
return DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
def normalize_text(text) -> Optional[str]:
|
||||
"""规范化文本:字符串或字符串数组"""
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
if isinstance(text, list):
|
||||
return ''.join(text)
|
||||
return None
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
if len(sys.argv) < 2:
|
||||
log_error('Task required')
|
||||
sys.exit(1)
|
||||
|
||||
# 检测是否为 resume 模式
|
||||
if sys.argv[1] == 'resume':
|
||||
if len(sys.argv) < 4:
|
||||
log_error('Resume mode requires: resume <session_id> <task>')
|
||||
sys.exit(1)
|
||||
return {
|
||||
'mode': 'resume',
|
||||
'session_id': sys.argv[2],
|
||||
'task': sys.argv[3],
|
||||
'model': sys.argv[4] if len(sys.argv) > 4 else DEFAULT_MODEL,
|
||||
'workdir': sys.argv[5] if len(sys.argv) > 5 else DEFAULT_WORKDIR
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'mode': 'new',
|
||||
'task': sys.argv[1],
|
||||
'model': sys.argv[2] if len(sys.argv) > 2 else DEFAULT_MODEL,
|
||||
'workdir': sys.argv[3] if len(sys.argv) > 3 else DEFAULT_WORKDIR
|
||||
}
|
||||
|
||||
|
||||
def build_codex_args(params: dict) -> list:
|
||||
"""构建 codex CLI 参数"""
|
||||
if params['mode'] == 'resume':
|
||||
return [
|
||||
'codex', 'e',
|
||||
'--skip-git-repo-check',
|
||||
'--json',
|
||||
'resume',
|
||||
params['session_id'],
|
||||
params['task']
|
||||
]
|
||||
else:
|
||||
return [
|
||||
'codex', 'e',
|
||||
'-m', params['model'],
|
||||
'--dangerously-bypass-approvals-and-sandbox',
|
||||
'--skip-git-repo-check',
|
||||
'-C', params['workdir'],
|
||||
'--json',
|
||||
params['task']
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
params = parse_args()
|
||||
codex_args = build_codex_args(params)
|
||||
timeout_sec = resolve_timeout()
|
||||
|
||||
thread_id: Optional[str] = None
|
||||
last_agent_message: Optional[str] = None
|
||||
|
||||
try:
|
||||
# 启动 codex 子进程
|
||||
process = subprocess.Popen(
|
||||
codex_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=sys.stderr, # 错误直接透传到 stderr
|
||||
text=True,
|
||||
bufsize=1 # 行缓冲
|
||||
)
|
||||
|
||||
# 逐行解析 JSON 输出
|
||||
for line in process.stdout:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
event = json.loads(line)
|
||||
|
||||
# 捕获 thread_id
|
||||
if event.get('type') == 'thread.started':
|
||||
thread_id = event.get('thread_id')
|
||||
|
||||
# 捕获 agent_message
|
||||
if (event.get('type') == 'item.completed' and
|
||||
event.get('item', {}).get('type') == 'agent_message'):
|
||||
text = normalize_text(event['item'].get('text'))
|
||||
if text:
|
||||
last_agent_message = text
|
||||
|
||||
except json.JSONDecodeError:
|
||||
log_warn(f"Failed to parse line: {line}")
|
||||
|
||||
# 等待进程结束
|
||||
returncode = process.wait(timeout=timeout_sec)
|
||||
|
||||
if returncode == 0:
|
||||
if last_agent_message:
|
||||
# 输出 agent_message
|
||||
sys.stdout.write(f"{last_agent_message}\n")
|
||||
|
||||
# 输出 session_id(如果存在)
|
||||
if thread_id:
|
||||
sys.stdout.write(f"\n---\nSESSION_ID: {thread_id}\n")
|
||||
|
||||
sys.exit(0)
|
||||
else:
|
||||
log_error('Codex completed without agent_message output')
|
||||
sys.exit(1)
|
||||
else:
|
||||
log_error(f'Codex exited with status {returncode}')
|
||||
sys.exit(returncode)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
log_error('Codex execution timeout')
|
||||
process.kill()
|
||||
try:
|
||||
process.wait(timeout=FORCE_KILL_DELAY)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
sys.exit(124)
|
||||
|
||||
except FileNotFoundError:
|
||||
log_error("codex command not found in PATH")
|
||||
sys.exit(127)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=FORCE_KILL_DELAY)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
sys.exit(130)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user