@trendai-crem/claude-skills 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -13
- package/cli.js +12 -3
- package/package.json +1 -1
- package/skills/codex/SKILL.md +190 -0
- package/skills/codex/scripts/ask_codex.ps1 +499 -0
- package/skills/codex/scripts/ask_codex.sh +364 -0
package/README.md
CHANGED
|
@@ -10,28 +10,51 @@ npx @trendai-crem/claude-skills
|
|
|
10
10
|
|
|
11
11
|
That's it. The command installs:
|
|
12
12
|
|
|
13
|
-
1. **External skills** — [superpowers](https://github.com/obra/superpowers))
|
|
13
|
+
1. **External skills** — [superpowers](https://github.com/obra/superpowers) (brainstorming, debugging, TDD, and more)
|
|
14
14
|
2. **Team skills** — custom skills maintained in this repo (override same-named externals)
|
|
15
15
|
|
|
16
16
|
## Update
|
|
17
17
|
|
|
18
|
-
Re-run the same command to update
|
|
18
|
+
Re-run the same command to get the latest version. If an update is available, you'll be notified automatically.
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx @trendai-crem/claude-skills
|
|
21
|
+
npx @trendai-crem/claude-skills@latest
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## Team Skills
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
26
|
+
### Engineering Quality
|
|
27
|
+
|
|
28
|
+
| Skill | Trigger | Description |
|
|
29
|
+
|-------|---------|-------------|
|
|
30
|
+
| **code-review** | "review my code", "code review" | Multi-perspective review — 5 reviewers + Codex baseline, enforces TM RDSec policy and Secure Coding Dojo checkpoints |
|
|
31
|
+
| **reviewing-prs** | "review this PR", "review pull request" | Structured PR review with 3 independent sub-agents covering correctness, security, and requirements |
|
|
32
|
+
|
|
33
|
+
### Atlassian
|
|
34
|
+
|
|
35
|
+
| Skill | Trigger | Description |
|
|
36
|
+
|-------|---------|-------------|
|
|
37
|
+
| **atlassian-tools** | Any Confluence/Jira URL or mention | Confluence wiki and Jira issue management — create, update, search pages and tickets |
|
|
38
|
+
| **wiki-generation** | "create a wiki page", "generate Confluence docs" | Generate Confluence documentation with proper ADF format and Mermaid diagram support |
|
|
39
|
+
|
|
40
|
+
### Google Style Guides
|
|
41
|
+
|
|
42
|
+
| Skill | Trigger | Description |
|
|
43
|
+
|-------|---------|-------------|
|
|
44
|
+
| **java** | "java style", "java coding standards" | Google Java Style Guide reference |
|
|
45
|
+
| **python** | "python style", "python coding standards" | Google Python Style Guide reference |
|
|
46
|
+
| **go** | "go style", "go coding standards" | Google Go Style Guide reference |
|
|
47
|
+
| **typescript** | "typescript style", "typescript coding standards" | Google TypeScript Style Guide reference |
|
|
48
|
+
| **javascript** | "javascript style", "javascript coding standards" | Google JavaScript Style Guide reference |
|
|
49
|
+
| **shell** | "shell style", "bash coding standards" | Google Shell Style Guide reference |
|
|
50
|
+
| **cpp** | "c++ style", "cpp coding standards" | Google C++ Style Guide reference |
|
|
29
51
|
|
|
30
52
|
## For Maintainers
|
|
31
53
|
|
|
32
54
|
### Adding a team skill
|
|
33
55
|
|
|
34
|
-
1. Create `
|
|
56
|
+
1. Create a branch: `git checkout -b feat/add-<skill-name>`
|
|
57
|
+
2. Add `skills/<skill-name>/SKILL.md` with frontmatter:
|
|
35
58
|
```markdown
|
|
36
59
|
---
|
|
37
60
|
name: skill-name
|
|
@@ -40,21 +63,23 @@ npx @trendai-crem/claude-skills
|
|
|
40
63
|
|
|
41
64
|
# Skill content here
|
|
42
65
|
```
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
3. Commit, push, and open a PR to `main`
|
|
67
|
+
4. Bump version in PR: `npm version minor`
|
|
68
|
+
5. Merging to `main` triggers automated publish via CI
|
|
45
69
|
|
|
46
|
-
###
|
|
70
|
+
### Version bumping
|
|
47
71
|
|
|
48
72
|
```bash
|
|
49
|
-
npm version patch # bug fixes
|
|
73
|
+
npm version patch # bug fixes / skill content updates
|
|
50
74
|
npm version minor # new skills added
|
|
51
75
|
npm version major # breaking changes
|
|
52
|
-
npm publish
|
|
53
76
|
```
|
|
54
77
|
|
|
78
|
+
Push to `main` — CI handles the publish automatically.
|
|
79
|
+
|
|
55
80
|
### External skill sources
|
|
56
81
|
|
|
57
|
-
Configured in `cli.js` under `EXTERNAL_SOURCES`.
|
|
82
|
+
Configured in `cli.js` under `EXTERNAL_SOURCES`. Edit the array and bump the version to add or remove external sources.
|
|
58
83
|
|
|
59
84
|
## Requirements
|
|
60
85
|
|
package/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
4
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { dirname, join } from 'path';
|
|
7
7
|
|
|
@@ -29,12 +29,21 @@ const externalResults = EXTERNAL_SOURCES.map(({ repo, flags, label }) =>
|
|
|
29
29
|
);
|
|
30
30
|
|
|
31
31
|
// 2. Team skills — required, overrides same-named externals
|
|
32
|
+
const teamSkills = readdirSync(join(__dir, 'skills'), { withFileTypes: true })
|
|
33
|
+
.filter(e => e.isDirectory())
|
|
34
|
+
.map(e => e.name)
|
|
35
|
+
.sort();
|
|
36
|
+
|
|
32
37
|
const teamOk = run(['skills', 'add', __dir, '--all', '-g', '-y'], 'team skills');
|
|
33
38
|
|
|
34
39
|
// Summary
|
|
35
40
|
console.log('\nResults:');
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
externalResults.forEach(({ label, ok }) => console.log(` ${ok ? '✓' : '✗'} ${label}`));
|
|
42
|
+
if (teamOk) {
|
|
43
|
+
teamSkills.forEach(name => console.log(` ✓ ${name}`));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(` ✗ team skills`);
|
|
46
|
+
}
|
|
38
47
|
|
|
39
48
|
if (!teamOk) {
|
|
40
49
|
console.error('\nFATAL: Team skills installation failed.');
|
package/package.json
CHANGED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex
|
|
3
|
+
description: Delegate coding tasks to Codex CLI for execution. Only invoke this skill when the user explicitly asks to use Codex — e.g., "用 codex 来做", "让 codex 执行", "ask codex to...", "codex 帮我写". Do not proactively delegate to Codex for general coding requests the user didn't specifically ask Codex to handle. Codex is an autonomous coding agent with the same tools as Claude (file read/write, grep, bash) — it explores the codebase and implements changes on its own. Claude's role is to understand the problem clearly and frame it well for Codex to execute.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Language
|
|
7
|
+
|
|
8
|
+
Respond in the same language the user is using. If the user writes in Chinese, respond in Chinese. If in English, respond in English.
|
|
9
|
+
|
|
10
|
+
## Prerequisites check
|
|
11
|
+
|
|
12
|
+
**Before anything else**, verify Codex CLI is installed and configured:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
command -v codex >/dev/null 2>&1 && echo "available" || echo "not found"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If Codex is **not found**, stop immediately and inform the user:
|
|
19
|
+
> "Codex CLI is not installed or not in PATH. Please install it first: `npm install -g @openai/codex`"
|
|
20
|
+
|
|
21
|
+
Do NOT attempt to proceed without a working Codex installation.
|
|
22
|
+
|
|
23
|
+
## Critical rules
|
|
24
|
+
|
|
25
|
+
- Use the bundled shell script rather than calling `codex` CLI directly — the script handles output capture, session tracking, and real-time progress streaming correctly.
|
|
26
|
+
- Run the script once per task. If it succeeds (exit code 0), read the output file and proceed. Don't re-run just because the output seems short — Codex often makes changes quietly without narrating every step.
|
|
27
|
+
- Quote file paths containing `[`, `]`, spaces, or special characters (e.g. `--file "src/app/[locale]/page.tsx"`). Without quotes, zsh treats `[...]` as a glob pattern and fails with "no matches found".
|
|
28
|
+
- **Keep the task prompt to the goal and constraints, not the implementation steps.** Aim for under ~500 words. Codex has the same tools as Claude and will explore the codebase itself — spelling out every file to change or every step tends to constrain it rather than help.
|
|
29
|
+
- **Don't paste file contents into the prompt.** Use `--file` to point Codex to key files — it reads them directly at their current version. Pasting contents wastes tokens and risks passing stale code.
|
|
30
|
+
- **Don't mention this skill or its configuration in the prompt.** Codex doesn't need to know about it.
|
|
31
|
+
|
|
32
|
+
## How to call the script
|
|
33
|
+
|
|
34
|
+
### Linux/macOS (bash)
|
|
35
|
+
|
|
36
|
+
The script path is:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
~/.claude/skills/codex/scripts/ask_codex.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Minimal invocation (with xhigh reasoning by default):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
~/.claude/skills/codex/scripts/ask_codex.sh "Your request in natural language" \
|
|
46
|
+
--reasoning xhigh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
With file context:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
~/.claude/skills/codex/scripts/ask_codex.sh "Refactor these components to use the new API" \
|
|
53
|
+
--reasoning xhigh \
|
|
54
|
+
--file src/components/UserList.tsx \
|
|
55
|
+
--file src/components/UserDetail.tsx
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Multi-turn conversation (continue a previous session):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
~/.claude/skills/codex/scripts/ask_codex.sh "Also add retry logic with exponential backoff" \
|
|
62
|
+
--reasoning xhigh \
|
|
63
|
+
--session <session_id from previous run>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Windows (PowerShell)
|
|
67
|
+
|
|
68
|
+
The script path is:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
~/.claude/skills/codex/scripts/ask_codex.ps1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Minimal invocation:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
& ~/.claude/skills/codex/scripts/ask_codex.ps1 "Your request in natural language" `
|
|
78
|
+
--reasoning xhigh
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Output format
|
|
82
|
+
|
|
83
|
+
The script prints on success:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
session_id=<thread_id>
|
|
87
|
+
output_path=<path to markdown file>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Read the file at `output_path` to get Codex's response. Save `session_id` if you plan follow-up calls.
|
|
91
|
+
|
|
92
|
+
## Workflow
|
|
93
|
+
|
|
94
|
+
### Step 1 — Understand the problem
|
|
95
|
+
|
|
96
|
+
Read the key files to grasp what's broken or needed. Focus on being able to describe the problem and goal clearly — you don't need to design the full solution. Codex will explore the codebase itself.
|
|
97
|
+
|
|
98
|
+
### Step 2 — Gather coding conventions
|
|
99
|
+
|
|
100
|
+
Before calling the script, collect the team's coding conventions and style guides to inject as context. Check in this order:
|
|
101
|
+
|
|
102
|
+
**a) Project-level conventions:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Check for CLAUDE.md or project rules
|
|
106
|
+
ls ~/.claude/CLAUDE.md 2>/dev/null
|
|
107
|
+
ls .claude/CLAUDE.md 2>/dev/null
|
|
108
|
+
ls .claude/rules/ 2>/dev/null
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If found, include relevant files via `--file`:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
--file ~/.claude/CLAUDE.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**b) Language-specific Google Style Guides:**
|
|
118
|
+
|
|
119
|
+
Check if style guide skills are installed for the language being used:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ls ~/.claude/skills/java/SKILL.md 2>/dev/null # Java
|
|
123
|
+
ls ~/.claude/skills/python/SKILL.md 2>/dev/null # Python
|
|
124
|
+
ls ~/.claude/skills/go/SKILL.md 2>/dev/null # Go
|
|
125
|
+
ls ~/.claude/skills/typescript/SKILL.md 2>/dev/null
|
|
126
|
+
ls ~/.claude/skills/javascript/SKILL.md 2>/dev/null
|
|
127
|
+
ls ~/.claude/skills/shell/SKILL.md 2>/dev/null
|
|
128
|
+
ls ~/.claude/skills/cpp/SKILL.md 2>/dev/null
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If the relevant style guide exists, include it:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
--file ~/.claude/skills/java/SKILL.md
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**c) No conventions found:**
|
|
138
|
+
|
|
139
|
+
If neither project rules nor style guides are found, skip this step entirely — let Codex discover conventions from the codebase on its own. Do not block or warn the user.
|
|
140
|
+
|
|
141
|
+
### Step 3 — Build and run the prompt
|
|
142
|
+
|
|
143
|
+
Construct the task description embedding the relevant conventions context, then run:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
~/.claude/skills/codex/scripts/ask_codex.sh "<task description with convention constraints>" \
|
|
147
|
+
--reasoning xhigh \
|
|
148
|
+
--file <entry-point files> \
|
|
149
|
+
--file <convention files if found>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Pass 1–4 entry-point files as starting hints. Codex will discover related files on its own.
|
|
153
|
+
|
|
154
|
+
For discussion or analysis without changes, use `--read-only`.
|
|
155
|
+
|
|
156
|
+
### Step 4 — Read and review
|
|
157
|
+
|
|
158
|
+
Read the output file, then review the changes in the workspace.
|
|
159
|
+
|
|
160
|
+
For multi-step projects, use `--session <id>` to continue with full conversation history. For independent parallel tasks, use the Task tool with `run_in_background: true`.
|
|
161
|
+
|
|
162
|
+
## Failure handling
|
|
163
|
+
|
|
164
|
+
- **`script: tcgetattr/ioctl: Operation not supported on socket`** (exit code 1): the script detects this automatically and falls back to direct execution. Update to the latest version if you still see this error.
|
|
165
|
+
- **Exit code 137**: the task was interrupted (user cancel or OOM). Not a Codex bug — retry or break the task into smaller pieces.
|
|
166
|
+
- **`ERROR codex_core::codex: failed to load skill ...`** in stderr: one of Codex's own installed skills has a broken YAML file. This warning is harmless — ignore it.
|
|
167
|
+
- **`(no response from codex)`** in the output file: Codex ran but produced no readable output. Check stderr for clues; the task may have hit a sandbox restriction.
|
|
168
|
+
|
|
169
|
+
## Options
|
|
170
|
+
|
|
171
|
+
- `--workspace <path>` — Target workspace directory (defaults to current directory).
|
|
172
|
+
- `--file <path>` — Point Codex to key entry-point files (repeatable, workspace-relative or absolute).
|
|
173
|
+
- `--session <id>` — Resume a previous session for multi-turn conversation.
|
|
174
|
+
- `--model <name>` — Override model (default: uses Codex config).
|
|
175
|
+
- `--reasoning <level>` — Reasoning effort: `low`, `medium`, `high`, `xhigh` (default: **`xhigh`**).
|
|
176
|
+
- `--sandbox <mode>` — Override sandbox policy (default: workspace-write via full-auto).
|
|
177
|
+
- `--read-only` — Read-only mode for pure discussion/analysis, no file changes.
|
|
178
|
+
|
|
179
|
+
## Resume mode limitations
|
|
180
|
+
|
|
181
|
+
When using `--session` to resume a previous conversation, note these limitations:
|
|
182
|
+
|
|
183
|
+
- **Must run in a git repository** — The `codex exec resume` command requires a git-trusted directory. It does not support `--skip-git-repo-check`.
|
|
184
|
+
- **Limited options** — Resume mode only supports `-c/--config` and `--last`. The following options are **not supported** in resume mode:
|
|
185
|
+
- `--sandbox`
|
|
186
|
+
- `--full-auto`
|
|
187
|
+
- `--read-only`
|
|
188
|
+
- `--model`
|
|
189
|
+
- `--workspace` (resumes in the original session's context)
|
|
190
|
+
- **Text output only** — Resume mode returns plain text instead of JSON-structured output.
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env powershell
|
|
2
|
+
# Windows PowerShell 5.1+ compatible script
|
|
3
|
+
[CmdletBinding()]
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Position = 0)]
|
|
6
|
+
[string]$Task,
|
|
7
|
+
|
|
8
|
+
[Alias('t')]
|
|
9
|
+
[string]$TaskText,
|
|
10
|
+
|
|
11
|
+
[Alias('w')]
|
|
12
|
+
[string]$Workspace = (Get-Location).Path,
|
|
13
|
+
|
|
14
|
+
[Alias('f')]
|
|
15
|
+
[string[]]$File,
|
|
16
|
+
|
|
17
|
+
[string]$Session,
|
|
18
|
+
|
|
19
|
+
[string]$Model,
|
|
20
|
+
|
|
21
|
+
[ValidateSet('low', 'medium', 'high')]
|
|
22
|
+
[string]$Reasoning = 'medium',
|
|
23
|
+
|
|
24
|
+
[string]$Sandbox,
|
|
25
|
+
|
|
26
|
+
[switch]$ReadOnly,
|
|
27
|
+
|
|
28
|
+
[switch]$FullAuto,
|
|
29
|
+
|
|
30
|
+
[Alias('o')]
|
|
31
|
+
[string]$Output,
|
|
32
|
+
|
|
33
|
+
[switch]$Help
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
$ErrorActionPreference = 'Stop'
|
|
37
|
+
|
|
38
|
+
function Show-Usage {
|
|
39
|
+
@'
|
|
40
|
+
Usage:
|
|
41
|
+
ask_codex.ps1 <task> [options]
|
|
42
|
+
ask_codex.ps1 -Task <task> [options]
|
|
43
|
+
|
|
44
|
+
Task input:
|
|
45
|
+
<task> First positional argument is the task text
|
|
46
|
+
-Task, -t <text> Alias for positional task
|
|
47
|
+
|
|
48
|
+
File context (optional, repeatable):
|
|
49
|
+
-File, -f <path> Priority file path
|
|
50
|
+
|
|
51
|
+
Multi-turn:
|
|
52
|
+
-Session <id> Resume a previous session (thread_id from prior run)
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
-Workspace, -w <path> Workspace directory (default: current directory)
|
|
56
|
+
-Model <name> Model override
|
|
57
|
+
-Reasoning <level> Reasoning effort: low, medium, high (default: medium)
|
|
58
|
+
-Sandbox <mode> Sandbox mode override
|
|
59
|
+
-ReadOnly Read-only sandbox (no file changes)
|
|
60
|
+
-FullAuto Full-auto mode (default)
|
|
61
|
+
-Output, -o <path> Output file path
|
|
62
|
+
-Help Show this help
|
|
63
|
+
|
|
64
|
+
Output (on success):
|
|
65
|
+
session_id=<thread_id> Use with -Session for follow-up calls
|
|
66
|
+
output_path=<file> Path to response markdown
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
# New task (positional)
|
|
70
|
+
ask_codex.ps1 "Add error handling to api.ts" -f src/api.ts
|
|
71
|
+
|
|
72
|
+
# With explicit workspace
|
|
73
|
+
ask_codex.ps1 "Fix the bug" -w C:\other\repo
|
|
74
|
+
|
|
75
|
+
# Continue conversation
|
|
76
|
+
ask_codex.ps1 "Also add retry logic" -Session <id>
|
|
77
|
+
'@
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Test-Command {
|
|
81
|
+
param([string]$Name)
|
|
82
|
+
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
|
83
|
+
Write-Error "[ERROR] Missing required command: $Name"
|
|
84
|
+
exit 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Trim-Whitespace {
|
|
89
|
+
param([string]$Text)
|
|
90
|
+
if ([string]::IsNullOrEmpty($Text)) { return '' }
|
|
91
|
+
return $Text.Trim() -replace '\s+', ' '
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Resolve-FileRef {
|
|
95
|
+
param(
|
|
96
|
+
[string]$Workspace,
|
|
97
|
+
[string]$RawPath
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
$cleaned = Trim-Whitespace $RawPath
|
|
101
|
+
if ([string]::IsNullOrWhiteSpace($cleaned)) { return '' }
|
|
102
|
+
|
|
103
|
+
# Remove line number suffixes (#L123 or :123-456)
|
|
104
|
+
$cleaned = $cleaned -replace '#L\d+$', ''
|
|
105
|
+
$cleaned = $cleaned -replace ':\d+(-\d+)?$', ''
|
|
106
|
+
|
|
107
|
+
# Make absolute if relative
|
|
108
|
+
if (-not [System.IO.Path]::IsPathRooted($cleaned)) {
|
|
109
|
+
$cleaned = Join-Path $Workspace $cleaned
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Normalize path
|
|
113
|
+
if (Test-Path $cleaned) {
|
|
114
|
+
return (Resolve-Path $cleaned -ErrorAction SilentlyContinue).Path
|
|
115
|
+
}
|
|
116
|
+
return $cleaned
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Write-File-NoBOM {
|
|
120
|
+
param([string]$Path, [string]$Content)
|
|
121
|
+
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
122
|
+
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Show help if requested
|
|
126
|
+
if ($Help) {
|
|
127
|
+
Show-Usage
|
|
128
|
+
exit 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Check required commands
|
|
132
|
+
Test-Command 'codex'
|
|
133
|
+
Test-Command 'jq'
|
|
134
|
+
|
|
135
|
+
# Resolve task text from either positional or named parameter
|
|
136
|
+
if ([string]::IsNullOrEmpty($Task) -and -not [string]::IsNullOrEmpty($TaskText)) {
|
|
137
|
+
$Task = $TaskText
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Validate workspace
|
|
141
|
+
if (-not (Test-Path $Workspace -PathType Container)) {
|
|
142
|
+
Write-Error "[ERROR] Workspace does not exist: $Workspace"
|
|
143
|
+
exit 1
|
|
144
|
+
}
|
|
145
|
+
$Workspace = (Resolve-Path $Workspace).Path
|
|
146
|
+
|
|
147
|
+
# Validate task
|
|
148
|
+
$Task = Trim-Whitespace $Task
|
|
149
|
+
if ([string]::IsNullOrEmpty($Task)) {
|
|
150
|
+
Write-Error "[ERROR] Request text is empty. Pass a positional arg or -Task."
|
|
151
|
+
exit 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Prepare output path
|
|
155
|
+
if ([string]::IsNullOrEmpty($Output)) {
|
|
156
|
+
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss')
|
|
157
|
+
$skillDir = Split-Path $PSScriptRoot -Parent
|
|
158
|
+
$runtimeDir = Join-Path $skillDir '.runtime'
|
|
159
|
+
if (-not (Test-Path $runtimeDir)) {
|
|
160
|
+
New-Item -ItemType Directory -Path $runtimeDir -Force | Out-Null
|
|
161
|
+
}
|
|
162
|
+
$Output = Join-Path $runtimeDir "$timestamp.md"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Build file context block
|
|
166
|
+
$fileBlock = ''
|
|
167
|
+
if ($File -and $File.Count -gt 0) {
|
|
168
|
+
$fileBlock = "`nPriority files (read these first before making changes):"
|
|
169
|
+
foreach ($ref in $File) {
|
|
170
|
+
$resolved = Resolve-FileRef -Workspace $Workspace -RawPath $ref
|
|
171
|
+
if (-not [string]::IsNullOrEmpty($resolved)) {
|
|
172
|
+
$existsTag = if (Test-Path $resolved) { 'exists' } else { 'missing' }
|
|
173
|
+
$fileBlock += "`n- $resolved ($existsTag)"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Build prompt
|
|
179
|
+
$prompt = $Task
|
|
180
|
+
if (-not [string]::IsNullOrEmpty($fileBlock)) {
|
|
181
|
+
$prompt += $fileBlock
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Build codex command
|
|
185
|
+
$codexArgs = @()
|
|
186
|
+
|
|
187
|
+
if (-not [string]::IsNullOrEmpty($Session)) {
|
|
188
|
+
# Resume mode: continue a previous session
|
|
189
|
+
# Note: resume only supports -c/--config and --last flags (no --json, --sandbox, etc.)
|
|
190
|
+
$codexArgs = @('exec', 'resume', '-c', "model_reasoning_effort=`"$Reasoning`"", '-c', 'skip_git_repo_check=true')
|
|
191
|
+
$codexArgs += $Session
|
|
192
|
+
} else {
|
|
193
|
+
# New session
|
|
194
|
+
$codexArgs = @('exec', '--cd', $Workspace, '--skip-git-repo-check', '--json', '-c', "model_reasoning_effort=`"$Reasoning`"")
|
|
195
|
+
if ($ReadOnly) {
|
|
196
|
+
$codexArgs += '--sandbox', 'read-only'
|
|
197
|
+
} elseif (-not [string]::IsNullOrEmpty($Sandbox)) {
|
|
198
|
+
$codexArgs += '--sandbox', $Sandbox
|
|
199
|
+
} elseif ($FullAuto) {
|
|
200
|
+
$codexArgs += '--full-auto'
|
|
201
|
+
}
|
|
202
|
+
if (-not [string]::IsNullOrEmpty($Model)) {
|
|
203
|
+
$codexArgs += '-m', $Model
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Create temp files
|
|
208
|
+
$tempDir = [System.IO.Path]::GetTempPath()
|
|
209
|
+
$guid = [guid]::NewGuid().ToString()
|
|
210
|
+
$stderrFile = Join-Path $tempDir "codex_stderr_$guid.txt"
|
|
211
|
+
$jsonFile = Join-Path $tempDir "codex_json_$guid.txt"
|
|
212
|
+
$promptFile = Join-Path $tempDir "codex_prompt_$guid.txt"
|
|
213
|
+
|
|
214
|
+
# Cleanup function
|
|
215
|
+
$cleanupScript = {
|
|
216
|
+
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
|
|
217
|
+
Remove-Item -Path $jsonFile -Force -ErrorAction SilentlyContinue
|
|
218
|
+
Remove-Item -Path $promptFile -Force -ErrorAction SilentlyContinue
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
# Write prompt to temp file (UTF-8 without BOM)
|
|
223
|
+
Write-File-NoBOM -Path $promptFile -Content $prompt
|
|
224
|
+
|
|
225
|
+
# Initialize json file
|
|
226
|
+
Write-File-NoBOM -Path $jsonFile -Content ''
|
|
227
|
+
|
|
228
|
+
# Setup process with async reading for real-time output
|
|
229
|
+
# On Windows, codex is installed as a .ps1 script, so we need to use cmd.exe or pwsh to run it
|
|
230
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
231
|
+
if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) {
|
|
232
|
+
# Use cmd.exe to run codex (works with .cmd/.ps1 wrappers)
|
|
233
|
+
$psi.FileName = 'cmd.exe'
|
|
234
|
+
$psi.Arguments = '/c codex ' + ($codexArgs -join ' ')
|
|
235
|
+
} else {
|
|
236
|
+
$psi.FileName = 'codex'
|
|
237
|
+
$psi.Arguments = $codexArgs -join ' '
|
|
238
|
+
}
|
|
239
|
+
$psi.WorkingDirectory = $Workspace
|
|
240
|
+
$psi.UseShellExecute = $false
|
|
241
|
+
$psi.RedirectStandardInput = $true
|
|
242
|
+
$psi.RedirectStandardOutput = $true
|
|
243
|
+
$psi.RedirectStandardError = $true
|
|
244
|
+
$psi.CreateNoWindow = $true
|
|
245
|
+
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
246
|
+
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
247
|
+
|
|
248
|
+
$process = New-Object System.Diagnostics.Process
|
|
249
|
+
$process.StartInfo = $psi
|
|
250
|
+
|
|
251
|
+
# StringBuilder for collecting output
|
|
252
|
+
$jsonOutput = New-Object System.Text.StringBuilder
|
|
253
|
+
$stderrOutput = New-Object System.Text.StringBuilder
|
|
254
|
+
$outputLock = New-Object Object
|
|
255
|
+
|
|
256
|
+
# Event handler script blocks
|
|
257
|
+
$jsonOutputRef = $jsonOutput
|
|
258
|
+
$stderrOutputRef = $stderrOutput
|
|
259
|
+
|
|
260
|
+
# Register event handlers for async reading
|
|
261
|
+
$isResumeMode = -not [string]::IsNullOrEmpty($Session)
|
|
262
|
+
$textOutput = New-Object System.Text.StringBuilder
|
|
263
|
+
|
|
264
|
+
$stdOutAction = {
|
|
265
|
+
param([object]$sender, [System.Diagnostics.DataReceivedEventArgs]$e)
|
|
266
|
+
if ($e.Data) {
|
|
267
|
+
$line = $e.Data
|
|
268
|
+
# Strip terminal artifacts
|
|
269
|
+
$line = $line -replace "`r", ''
|
|
270
|
+
$line = $line -replace [char]4, ''
|
|
271
|
+
|
|
272
|
+
if (-not [string]::IsNullOrEmpty($line)) {
|
|
273
|
+
if ($line.StartsWith('{')) {
|
|
274
|
+
# JSON line (new session mode)
|
|
275
|
+
[System.Threading.Monitor]::Enter($Event.MessageData)
|
|
276
|
+
try {
|
|
277
|
+
$Event.MessageData.AppendLine($line) | Out-Null
|
|
278
|
+
} finally {
|
|
279
|
+
[System.Threading.Monitor]::Exit($Event.MessageData)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Print progress for relevant events
|
|
283
|
+
if ($line -match '"item\.started"' -or $line -match '"item\.completed"') {
|
|
284
|
+
if ($line -match '"item\.started"' -and $line -match '"command_execution"') {
|
|
285
|
+
try {
|
|
286
|
+
$json = $line | ConvertFrom-Json -ErrorAction SilentlyContinue
|
|
287
|
+
$cmd = $json.item.command
|
|
288
|
+
if ($cmd) {
|
|
289
|
+
$cmd = $cmd -replace '^/bin/(zsh|bash) (-lc|-c) ', ''
|
|
290
|
+
if ($cmd.Length -gt 100) { $cmd = $cmd.Substring(0, 100) }
|
|
291
|
+
Write-Host "[codex] > $cmd" -ForegroundColor Gray
|
|
292
|
+
}
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
295
|
+
if ($line -match '"item\.completed"' -and $line -match '"agent_message"') {
|
|
296
|
+
try {
|
|
297
|
+
$json = $line | ConvertFrom-Json -ErrorAction SilentlyContinue
|
|
298
|
+
$text = $json.item.text
|
|
299
|
+
if ($text) {
|
|
300
|
+
$preview = $text.Split("`n")[0]
|
|
301
|
+
if ($preview.Length -gt 120) { $preview = $preview.Substring(0, 120) }
|
|
302
|
+
Write-Host "[codex] $preview" -ForegroundColor Gray
|
|
303
|
+
}
|
|
304
|
+
} catch {}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
# Plain text line (resume mode)
|
|
309
|
+
[System.Threading.Monitor]::Enter($Event.MessageData)
|
|
310
|
+
try {
|
|
311
|
+
$Event.MessageData.AppendLine($line) | Out-Null
|
|
312
|
+
} finally {
|
|
313
|
+
[System.Threading.Monitor]::Exit($Event.MessageData)
|
|
314
|
+
}
|
|
315
|
+
# Show progress for text output
|
|
316
|
+
$preview = $line
|
|
317
|
+
if ($preview.Length -gt 120) { $preview = $preview.Substring(0, 120) }
|
|
318
|
+
Write-Host "[codex] $preview" -ForegroundColor Gray
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
$stdErrAction = {
|
|
325
|
+
param([object]$sender, [System.Diagnostics.DataReceivedEventArgs]$e)
|
|
326
|
+
if ($e.Data) {
|
|
327
|
+
[System.Threading.Monitor]::Enter($Event.MessageData)
|
|
328
|
+
try {
|
|
329
|
+
$Event.MessageData.AppendLine($e.Data) | Out-Null
|
|
330
|
+
} finally {
|
|
331
|
+
[System.Threading.Monitor]::Exit($Event.MessageData)
|
|
332
|
+
}
|
|
333
|
+
Write-Host $e.Data -ForegroundColor Yellow
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Register events - use textOutput for resume mode, jsonOutput for new session
|
|
338
|
+
$outputData = if ($isResumeMode) { $textOutput } else { $jsonOutput }
|
|
339
|
+
$stdOutEvent = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -Action $stdOutAction -MessageData $outputData
|
|
340
|
+
$stdErrEvent = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action $stdErrAction -MessageData $stderrOutput
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
# Start process
|
|
344
|
+
$process.Start() | Out-Null
|
|
345
|
+
|
|
346
|
+
# Begin async reading
|
|
347
|
+
$process.BeginOutputReadLine()
|
|
348
|
+
$process.BeginErrorReadLine()
|
|
349
|
+
|
|
350
|
+
# Write prompt to stdin
|
|
351
|
+
$process.StandardInput.Write($prompt)
|
|
352
|
+
$process.StandardInput.Close()
|
|
353
|
+
|
|
354
|
+
# Wait for process to exit
|
|
355
|
+
$process.WaitForExit()
|
|
356
|
+
$exitCode = $process.ExitCode
|
|
357
|
+
|
|
358
|
+
} finally {
|
|
359
|
+
# Unregister events
|
|
360
|
+
Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction SilentlyContinue
|
|
361
|
+
Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction SilentlyContinue
|
|
362
|
+
$process.Dispose()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Process output based on mode
|
|
366
|
+
$threadId = $null
|
|
367
|
+
$outputContent = @()
|
|
368
|
+
|
|
369
|
+
if ($isResumeMode) {
|
|
370
|
+
# Resume mode: plain text output
|
|
371
|
+
$textContent = $textOutput.ToString().Trim()
|
|
372
|
+
|
|
373
|
+
# Check for errors
|
|
374
|
+
$stderrText = $stderrOutput.ToString()
|
|
375
|
+
$hasValidOutput = -not [string]::IsNullOrWhiteSpace($textContent)
|
|
376
|
+
|
|
377
|
+
if ($stderrText -match '\[ERROR\]' -and -not $hasValidOutput) {
|
|
378
|
+
Write-Error "[ERROR] Codex command failed"
|
|
379
|
+
Write-Error $stderrText
|
|
380
|
+
exit 1
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if ($exitCode -ne 0 -and -not $hasValidOutput) {
|
|
384
|
+
Write-Error "[ERROR] Codex exited with code $exitCode"
|
|
385
|
+
exit 1
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Use session ID from parameter
|
|
389
|
+
$threadId = $Session
|
|
390
|
+
if (-not [string]::IsNullOrWhiteSpace($textContent)) {
|
|
391
|
+
$outputContent += $textContent
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
# New session mode: JSON output
|
|
395
|
+
$jsonText = $jsonOutput.ToString()
|
|
396
|
+
Write-File-NoBOM -Path $jsonFile -Content $jsonText
|
|
397
|
+
|
|
398
|
+
# Check for errors - but only fail if no valid output was received
|
|
399
|
+
$stderrText = $stderrOutput.ToString()
|
|
400
|
+
$hasValidOutput = -not [string]::IsNullOrWhiteSpace($jsonText) -and $jsonText -match '"thread_id"'
|
|
401
|
+
|
|
402
|
+
if ($stderrText -match '\[ERROR\]' -and -not $hasValidOutput) {
|
|
403
|
+
Write-Error "[ERROR] Codex command failed"
|
|
404
|
+
Write-Error $stderrText
|
|
405
|
+
exit 1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if ($exitCode -ne 0 -and -not $hasValidOutput) {
|
|
409
|
+
Write-Error "[ERROR] Codex exited with code $exitCode"
|
|
410
|
+
exit 1
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# Extract thread_id and messages from JSON stream
|
|
414
|
+
if (-not [string]::IsNullOrWhiteSpace($jsonText)) {
|
|
415
|
+
# Find thread_id
|
|
416
|
+
if ($jsonText -match '"thread_id"\s*:\s*"([^"]+)"') {
|
|
417
|
+
$threadId = $matches[1]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Parse JSON lines using PowerShell native parsing (more reliable on Windows)
|
|
421
|
+
$jsonLines = $jsonText -split "`n" | Where-Object { $_.Trim() -and $_.TrimStart().StartsWith('{') }
|
|
422
|
+
|
|
423
|
+
foreach ($line in $jsonLines) {
|
|
424
|
+
try {
|
|
425
|
+
$obj = $line | ConvertFrom-Json -ErrorAction SilentlyContinue
|
|
426
|
+
if (-not $obj) { continue }
|
|
427
|
+
|
|
428
|
+
# Process completed items
|
|
429
|
+
if ($obj.type -eq 'item.completed' -and $obj.item) {
|
|
430
|
+
$item = $obj.item
|
|
431
|
+
|
|
432
|
+
# Agent messages
|
|
433
|
+
if ($item.type -eq 'agent_message' -and $item.text) {
|
|
434
|
+
$outputContent += $item.text
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Command executions
|
|
438
|
+
if ($item.type -eq 'command_execution' -and $item.command) {
|
|
439
|
+
$cmd = $item.command -replace '^/bin/(zsh|bash) (-lc|-c) ', ''
|
|
440
|
+
$cmdPreview = $cmd.Substring(0, [Math]::Min(200, $cmd.Length))
|
|
441
|
+
$outPreview = ''
|
|
442
|
+
if ($item.aggregated_output) {
|
|
443
|
+
$outPreview = $item.aggregated_output.Substring(0, [Math]::Min(500, $item.aggregated_output.Length))
|
|
444
|
+
}
|
|
445
|
+
$outputContent += "### Shell: ``$cmdPreview```n$outPreview"
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Tool calls (file operations)
|
|
449
|
+
if ($item.type -eq 'tool_call' -and $item.name) {
|
|
450
|
+
$args = $null
|
|
451
|
+
try {
|
|
452
|
+
$args = $item.arguments | ConvertFrom-Json -ErrorAction SilentlyContinue
|
|
453
|
+
} catch {}
|
|
454
|
+
|
|
455
|
+
if ($item.name -eq 'write_file' -and $args.path) {
|
|
456
|
+
$outputContent += "### File written: $($args.path)"
|
|
457
|
+
}
|
|
458
|
+
if ($item.name -eq 'patch_file' -and $args.path) {
|
|
459
|
+
$outputContent += "### File patched: $($args.path)"
|
|
460
|
+
}
|
|
461
|
+
if ($item.name -eq 'shell' -and $args.command) {
|
|
462
|
+
$cmdPreview = $args.command.Substring(0, [Math]::Min(200, $args.command.Length))
|
|
463
|
+
$outPreview = ''
|
|
464
|
+
if ($item.output) {
|
|
465
|
+
$outPreview = $item.output.Substring(0, [Math]::Min(500, $item.output.Length))
|
|
466
|
+
}
|
|
467
|
+
$outputContent += "### Shell: ``$cmdPreview```n$outPreview"
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
# Skip malformed lines
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
# Ensure output directory exists
|
|
479
|
+
$outputDir = Split-Path $Output -Parent
|
|
480
|
+
if (-not (Test-Path $outputDir)) {
|
|
481
|
+
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
# Write output
|
|
485
|
+
if ($outputContent.Count -gt 0) {
|
|
486
|
+
Write-File-NoBOM -Path $Output -Content ($outputContent -join "`n")
|
|
487
|
+
} else {
|
|
488
|
+
Write-File-NoBOM -Path $Output -Content "(no response from codex)"
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# Output results
|
|
492
|
+
if (-not [string]::IsNullOrEmpty($threadId)) {
|
|
493
|
+
Write-Output "session_id=$threadId"
|
|
494
|
+
}
|
|
495
|
+
Write-Output "output_path=$Output"
|
|
496
|
+
|
|
497
|
+
} finally {
|
|
498
|
+
& $cleanupScript
|
|
499
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'USAGE'
|
|
6
|
+
Usage:
|
|
7
|
+
ask_codex.sh <task> [options]
|
|
8
|
+
ask_codex.sh -t <task> [options]
|
|
9
|
+
|
|
10
|
+
Task input:
|
|
11
|
+
<task> First positional argument is the task text
|
|
12
|
+
-t, --task <text> Alias for positional task (backward compat)
|
|
13
|
+
(stdin) Pipe task text via stdin if no arg/flag given
|
|
14
|
+
|
|
15
|
+
File context (optional, repeatable):
|
|
16
|
+
-f, --file <path> Priority file path
|
|
17
|
+
|
|
18
|
+
Multi-turn:
|
|
19
|
+
--session <id> Resume a previous session (thread_id from prior run)
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-w, --workspace <path> Workspace directory (default: current directory)
|
|
23
|
+
--model <name> Model override
|
|
24
|
+
--reasoning <level> Reasoning effort: low, medium, high (default: medium)
|
|
25
|
+
--sandbox <mode> Sandbox mode override
|
|
26
|
+
--read-only Read-only sandbox (no file changes)
|
|
27
|
+
--full-auto Full-auto mode (default)
|
|
28
|
+
-o, --output <path> Output file path
|
|
29
|
+
-h, --help Show this help
|
|
30
|
+
|
|
31
|
+
Output (on success):
|
|
32
|
+
session_id=<thread_id> Use with --session for follow-up calls
|
|
33
|
+
output_path=<file> Path to response markdown
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
# New task (positional)
|
|
37
|
+
ask_codex.sh "Add error handling to api.ts" -f src/api.ts
|
|
38
|
+
|
|
39
|
+
# With explicit workspace
|
|
40
|
+
ask_codex.sh "Fix the bug" -w /other/repo
|
|
41
|
+
|
|
42
|
+
# Continue conversation
|
|
43
|
+
ask_codex.sh "Also add retry logic" --session <id>
|
|
44
|
+
USAGE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
require_cmd() {
|
|
48
|
+
if ! command -v "$1" >/dev/null 2>&1; then
|
|
49
|
+
echo "[ERROR] Missing required command: $1" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
trim_whitespace() {
|
|
55
|
+
awk 'BEGIN { RS=""; ORS="" } { gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, ""); print }' <<<"$1"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
to_abs_if_exists() {
|
|
59
|
+
local target="$1"
|
|
60
|
+
if [[ -e "$target" ]]; then
|
|
61
|
+
local dir
|
|
62
|
+
dir="$(cd "$(dirname "$target")" && pwd)"
|
|
63
|
+
echo "$dir/$(basename "$target")"
|
|
64
|
+
return
|
|
65
|
+
fi
|
|
66
|
+
echo "$target"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve_file_ref() {
|
|
70
|
+
local workspace="$1" raw="$2" cleaned
|
|
71
|
+
cleaned="$(trim_whitespace "$raw")"
|
|
72
|
+
[[ -z "$cleaned" ]] && { echo ""; return; }
|
|
73
|
+
if [[ "$cleaned" =~ ^(.+)#L[0-9]+$ ]]; then cleaned="${BASH_REMATCH[1]}"; fi
|
|
74
|
+
if [[ "$cleaned" =~ ^(.+):[0-9]+(-[0-9]+)?$ ]]; then cleaned="${BASH_REMATCH[1]}"; fi
|
|
75
|
+
if [[ "$cleaned" != /* ]]; then cleaned="$workspace/$cleaned"; fi
|
|
76
|
+
to_abs_if_exists "$cleaned"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
append_file_refs() {
|
|
80
|
+
local raw="$1" item
|
|
81
|
+
IFS=',' read -r -a items <<< "$raw"
|
|
82
|
+
for item in "${items[@]}"; do
|
|
83
|
+
local trimmed
|
|
84
|
+
trimmed="$(trim_whitespace "$item")"
|
|
85
|
+
[[ -n "$trimmed" ]] && file_refs+=("$trimmed")
|
|
86
|
+
done
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# --- Parse arguments ---
|
|
90
|
+
|
|
91
|
+
workspace="${PWD}"
|
|
92
|
+
task_text=""
|
|
93
|
+
model=""
|
|
94
|
+
reasoning_effort=""
|
|
95
|
+
sandbox_mode=""
|
|
96
|
+
read_only=false
|
|
97
|
+
full_auto=true
|
|
98
|
+
output_path=""
|
|
99
|
+
session_id=""
|
|
100
|
+
file_refs=()
|
|
101
|
+
|
|
102
|
+
while [[ $# -gt 0 ]]; do
|
|
103
|
+
case "$1" in
|
|
104
|
+
-w|--workspace) workspace="${2:-}"; shift 2 ;;
|
|
105
|
+
-t|--task) task_text="${2:-}"; shift 2 ;;
|
|
106
|
+
-f|--file|--focus) append_file_refs "${2:-}"; shift 2 ;;
|
|
107
|
+
--model) model="${2:-}"; shift 2 ;;
|
|
108
|
+
--reasoning) reasoning_effort="${2:-}"; shift 2 ;;
|
|
109
|
+
--sandbox) sandbox_mode="${2:-}"; full_auto=false; shift 2 ;;
|
|
110
|
+
--read-only) read_only=true; full_auto=false; shift ;;
|
|
111
|
+
--full-auto) full_auto=true; shift ;;
|
|
112
|
+
--session) session_id="${2:-}"; shift 2 ;;
|
|
113
|
+
-o|--output) output_path="${2:-}"; shift 2 ;;
|
|
114
|
+
-h|--help) usage; exit 0 ;;
|
|
115
|
+
-*) echo "[ERROR] Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
|
116
|
+
*) if [[ -z "$task_text" ]]; then task_text="$1"; shift; else echo "[ERROR] Unexpected argument: $1" >&2; usage >&2; exit 1; fi ;;
|
|
117
|
+
esac
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
require_cmd codex
|
|
121
|
+
require_cmd jq
|
|
122
|
+
|
|
123
|
+
# --- Validate inputs ---
|
|
124
|
+
|
|
125
|
+
if [[ ! -d "$workspace" ]]; then
|
|
126
|
+
echo "[ERROR] Workspace does not exist: $workspace" >&2; exit 1
|
|
127
|
+
fi
|
|
128
|
+
workspace="$(cd "$workspace" && pwd)"
|
|
129
|
+
|
|
130
|
+
if [[ -z "$task_text" && ! -t 0 ]]; then
|
|
131
|
+
task_text="$(cat)"
|
|
132
|
+
fi
|
|
133
|
+
task_text="$(trim_whitespace "$task_text")"
|
|
134
|
+
|
|
135
|
+
if [[ -z "$task_text" ]]; then
|
|
136
|
+
echo "[ERROR] Request text is empty. Pass a positional arg, --task, or stdin." >&2; exit 1
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# --- Prepare output path ---
|
|
140
|
+
|
|
141
|
+
if [[ -z "$output_path" ]]; then
|
|
142
|
+
timestamp="$(date -u +"%Y%m%d-%H%M%S")"
|
|
143
|
+
skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
144
|
+
output_path="$skill_dir/.runtime/${timestamp}.md"
|
|
145
|
+
fi
|
|
146
|
+
mkdir -p "$(dirname "$output_path")"
|
|
147
|
+
|
|
148
|
+
# --- Build file context block ---
|
|
149
|
+
|
|
150
|
+
file_block=""
|
|
151
|
+
if (( ${#file_refs[@]} > 0 )); then
|
|
152
|
+
file_block=$'\nPriority files (read these first before making changes):'
|
|
153
|
+
for ref in "${file_refs[@]}"; do
|
|
154
|
+
resolved="$(resolve_file_ref "$workspace" "$ref")"
|
|
155
|
+
[[ -z "$resolved" ]] && continue
|
|
156
|
+
exists_tag="missing"
|
|
157
|
+
[[ -e "$resolved" ]] && exists_tag="exists"
|
|
158
|
+
file_block+=$'\n- '"${resolved} (${exists_tag})"
|
|
159
|
+
done
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# --- Build prompt ---
|
|
163
|
+
|
|
164
|
+
prompt="$task_text"
|
|
165
|
+
if [[ -n "$file_block" ]]; then
|
|
166
|
+
prompt+=$'\n'"$file_block"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# --- Determine reasoning effort ---
|
|
170
|
+
|
|
171
|
+
if [[ -z "$reasoning_effort" ]]; then
|
|
172
|
+
reasoning_effort="medium"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# --- Build codex command ---
|
|
176
|
+
|
|
177
|
+
if [[ -n "$session_id" ]]; then
|
|
178
|
+
# Resume mode: continue a previous session
|
|
179
|
+
# Note: resume only supports -c/--config and --last flags (no --json, --sandbox, etc.)
|
|
180
|
+
cmd=(codex exec resume -c "model_reasoning_effort=\"$reasoning_effort\"" -c "skip_git_repo_check=true")
|
|
181
|
+
cmd+=("$session_id")
|
|
182
|
+
else
|
|
183
|
+
# New session
|
|
184
|
+
cmd=(codex exec --cd "$workspace" --skip-git-repo-check --json -c "model_reasoning_effort=\"$reasoning_effort\"")
|
|
185
|
+
if [[ "$read_only" == true ]]; then
|
|
186
|
+
cmd+=(--sandbox read-only)
|
|
187
|
+
elif [[ -n "$sandbox_mode" ]]; then
|
|
188
|
+
cmd+=(--sandbox "$sandbox_mode")
|
|
189
|
+
elif [[ "$full_auto" == true ]]; then
|
|
190
|
+
cmd+=(--full-auto)
|
|
191
|
+
fi
|
|
192
|
+
[[ -n "$model" ]] && cmd+=(-m "$model")
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# --- Progress watcher function ---
|
|
196
|
+
|
|
197
|
+
print_progress() {
|
|
198
|
+
local line="$1"
|
|
199
|
+
local item_type cmd_str preview
|
|
200
|
+
# Fast string checks before calling jq
|
|
201
|
+
case "$line" in
|
|
202
|
+
*'"item.started"'*'"command_execution"'*)
|
|
203
|
+
cmd_str=$(printf '%s' "$line" | jq -r '.item.command // empty' 2>/dev/null | sed 's|^/bin/zsh -lc ||; s|^/bin/bash -c ||' | cut -c1-100)
|
|
204
|
+
[[ -n "$cmd_str" ]] && echo "[codex] > $cmd_str" >&2
|
|
205
|
+
;;
|
|
206
|
+
*'"item.completed"'*'"agent_message"'*)
|
|
207
|
+
preview=$(printf '%s' "$line" | jq -r '.item.text // empty' 2>/dev/null | head -1 | cut -c1-120)
|
|
208
|
+
[[ -n "$preview" ]] && echo "[codex] $preview" >&2
|
|
209
|
+
;;
|
|
210
|
+
esac
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# --- Execute and capture output ---
|
|
214
|
+
|
|
215
|
+
stderr_file="$(mktemp)"
|
|
216
|
+
json_file="$(mktemp)"
|
|
217
|
+
text_file="$(mktemp)"
|
|
218
|
+
prompt_file="$(mktemp)"
|
|
219
|
+
trap 'rm -f "$stderr_file" "$json_file" "$text_file" "$prompt_file"' EXIT
|
|
220
|
+
|
|
221
|
+
# Write prompt to a temp file and pipe from there to avoid shell argument
|
|
222
|
+
# length issues and encoding problems with very long or multi-byte prompts.
|
|
223
|
+
printf "%s" "$prompt" > "$prompt_file"
|
|
224
|
+
|
|
225
|
+
# Run codex and capture its output.
|
|
226
|
+
# We prefer `script` to allocate a pseudo-TTY, which forces codex to line-buffer
|
|
227
|
+
# its output so progress events arrive in real time. However, `script` requires a
|
|
228
|
+
# real controlling terminal and fails with "tcgetattr/ioctl: Operation not supported
|
|
229
|
+
# on socket" in socket-based environments (e.g. some Claude Code sandboxes). We
|
|
230
|
+
# detect this upfront and fall back to direct execution — output may arrive all at
|
|
231
|
+
# once at the end, but the task still completes correctly.
|
|
232
|
+
run_codex() {
|
|
233
|
+
# BSD script (macOS): script [-q] [file [command...]]
|
|
234
|
+
# util-linux script (Linux): script [-q] -c <command> [file]
|
|
235
|
+
# Probe the local variant and use matching syntax for PTY allocation.
|
|
236
|
+
# Falls back to direct execution if neither probe succeeds (e.g. socket stdin).
|
|
237
|
+
local os
|
|
238
|
+
os="$(uname -s)"
|
|
239
|
+
if [[ "$os" == "Darwin" ]]; then
|
|
240
|
+
if script -q /dev/null true >/dev/null 2>&1; then
|
|
241
|
+
script -q /dev/null /bin/bash -c \
|
|
242
|
+
"cd $(printf '%q' "$workspace") && $(printf '%q ' "${cmd[@]}") < $(printf '%q' "$prompt_file") 2>$(printf '%q' "$stderr_file")"
|
|
243
|
+
return
|
|
244
|
+
fi
|
|
245
|
+
else
|
|
246
|
+
if script -q -c "true" /dev/null >/dev/null 2>&1; then
|
|
247
|
+
script -q -c \
|
|
248
|
+
"cd $(printf '%q' "$workspace") && $(printf '%q ' "${cmd[@]}") < $(printf '%q' "$prompt_file") 2>$(printf '%q' "$stderr_file")" \
|
|
249
|
+
/dev/null
|
|
250
|
+
return
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
# Fallback: direct execution (no PTY; progress events arrive in batch)
|
|
254
|
+
(cd "$workspace" && "${cmd[@]}" < "$prompt_file" 2>"$stderr_file")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if [[ -n "$session_id" ]]; then
|
|
258
|
+
# Resume mode: plain text output (no JSON support)
|
|
259
|
+
run_codex | while IFS= read -r line; do
|
|
260
|
+
# Strip terminal artifacts (carriage return, ^D EOF marker)
|
|
261
|
+
cleaned="${line//$'\r'/}"
|
|
262
|
+
cleaned="${cleaned//$'\004'/}"
|
|
263
|
+
[[ -z "$cleaned" ]] && continue
|
|
264
|
+
# Write to text_file for later output
|
|
265
|
+
printf '%s\n' "$cleaned" >> "$text_file"
|
|
266
|
+
# Print progress
|
|
267
|
+
preview="${cleaned:0:120}"
|
|
268
|
+
echo "[codex] $preview" >&2
|
|
269
|
+
done
|
|
270
|
+
else
|
|
271
|
+
# New session: JSON output
|
|
272
|
+
run_codex | while IFS= read -r line; do
|
|
273
|
+
# Strip terminal artifacts (carriage return, ^D EOF marker)
|
|
274
|
+
cleaned="${line//$'\r'/}"
|
|
275
|
+
cleaned="${cleaned//$'\004'/}"
|
|
276
|
+
[[ -z "$cleaned" ]] && continue
|
|
277
|
+
# Only process JSON lines (must start with '{')
|
|
278
|
+
[[ "$cleaned" != \{* ]] && continue
|
|
279
|
+
# Write to json_file for later parsing
|
|
280
|
+
printf '%s\n' "$cleaned" >> "$json_file"
|
|
281
|
+
# Only parse progress-relevant events (fast string check before jq)
|
|
282
|
+
case "$cleaned" in
|
|
283
|
+
*'"item.started"'*|*'"item.completed"'*) print_progress "$cleaned" ;;
|
|
284
|
+
esac
|
|
285
|
+
done
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
if [[ -s "$stderr_file" ]] && grep -q '\[ERROR\]' "$stderr_file" 2>/dev/null; then
|
|
289
|
+
echo "[ERROR] Codex command failed" >&2
|
|
290
|
+
cat "$stderr_file" >&2
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [[ -s "$stderr_file" ]]; then
|
|
295
|
+
cat "$stderr_file" >&2
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# --- Process output based on mode ---
|
|
299
|
+
|
|
300
|
+
if [[ -n "$session_id" ]]; then
|
|
301
|
+
# Resume mode: use plain text output
|
|
302
|
+
thread_id="$session_id"
|
|
303
|
+
if [[ -s "$text_file" ]]; then
|
|
304
|
+
cat "$text_file" > "$output_path"
|
|
305
|
+
else
|
|
306
|
+
echo "(no response from codex)" > "$output_path"
|
|
307
|
+
fi
|
|
308
|
+
else
|
|
309
|
+
# New session: Extract thread_id and all messages from JSON stream
|
|
310
|
+
thread_id="$(jq -r 'select(.type == "thread.started") | .thread_id' < "$json_file" | head -1)"
|
|
311
|
+
|
|
312
|
+
# Collect all completed items: file changes, tool calls, and agent messages.
|
|
313
|
+
# This gives full visibility into what codex actually did, not just the last message.
|
|
314
|
+
{
|
|
315
|
+
# 1. Show command executions — skip pure file-reading/searching commands.
|
|
316
|
+
# Codex explores the codebase heavily (sed/cat/nl/rg/grep/awk/wc/find/ls), but
|
|
317
|
+
# those reads produce no signal for Claude Code — it can read files directly if needed.
|
|
318
|
+
# Keep build, test, git, and mutation commands that reflect actual work done.
|
|
319
|
+
#
|
|
320
|
+
# Note: zsh wraps commands in quotes, so after stripping the shell prefix the
|
|
321
|
+
# command may start with " or ' — the regex accounts for this with [\"']?.
|
|
322
|
+
jq -r '
|
|
323
|
+
select(.type == "item.completed" and .item.type == "command_execution")
|
|
324
|
+
| .item
|
|
325
|
+
| ((.command // "") | gsub("^/bin/zsh -lc "; "") | gsub("^/bin/bash -c "; "")) as $cmd
|
|
326
|
+
| select($cmd | test("^[\"'"'"']?(sed |cat |head |tail |nl |rg |grep |awk |wc |find |ls )") | not)
|
|
327
|
+
| "### Shell: `" + ($cmd[0:200]) + "`\n" + (.aggregated_output // "" | .[0:500])
|
|
328
|
+
' < "$json_file" 2>/dev/null
|
|
329
|
+
|
|
330
|
+
# 2. Show file write/patch operations (tool_call style, if any)
|
|
331
|
+
jq -r '
|
|
332
|
+
select(.type == "item.completed" and .item.type == "tool_call")
|
|
333
|
+
| .item
|
|
334
|
+
| if .name == "write_file" then
|
|
335
|
+
"### File written: " + (.arguments | fromjson | .path // "unknown")
|
|
336
|
+
elif .name == "patch_file" then
|
|
337
|
+
"### File patched: " + (.arguments | fromjson | .path // "unknown")
|
|
338
|
+
elif .name == "shell" then
|
|
339
|
+
"### Shell: `" + (.arguments | fromjson | .command // "unknown")[0:200] + "`\n" + (.output // "" | .[0:500])
|
|
340
|
+
else empty
|
|
341
|
+
end
|
|
342
|
+
' < "$json_file" 2>/dev/null
|
|
343
|
+
|
|
344
|
+
# 3. Show all agent messages. Short messages (lint results, "tests failed",
|
|
345
|
+
# "no changes needed") carry high signal and must not be dropped by a length
|
|
346
|
+
# threshold. In practice, Codex tends to emit a small number of large blocks
|
|
347
|
+
# rather than many tiny fragments, so this produces clean output without filtering.
|
|
348
|
+
jq -r '
|
|
349
|
+
select(.type == "item.completed" and .item.type == "agent_message") | .item.text
|
|
350
|
+
' < "$json_file" 2>/dev/null
|
|
351
|
+
} > "$output_path"
|
|
352
|
+
|
|
353
|
+
# If nothing was captured, write a fallback
|
|
354
|
+
if [[ ! -s "$output_path" ]]; then
|
|
355
|
+
echo "(no response from codex)" > "$output_path"
|
|
356
|
+
fi
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# --- Output results ---
|
|
360
|
+
|
|
361
|
+
if [[ -n "$thread_id" ]]; then
|
|
362
|
+
echo "session_id=$thread_id"
|
|
363
|
+
fi
|
|
364
|
+
echo "output_path=$output_path"
|