wile 0.4.16 → 0.4.18

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 CHANGED
@@ -25,6 +25,8 @@ This creates:
25
25
 
26
26
  Set `WILE_REPO_SOURCE=local` in `.wile/secrets/.env` to run against the current directory without GitHub.
27
27
  When `WILE_REPO_SOURCE=local`, GitHub credentials are optional.
28
+ Set `WILE_MAX_ITERATIONS` in `.wile/secrets/.env` to change the default loop limit (default: 25).
29
+ Set `CODING_AGENT=OC` to use OpenCode (OpenRouter), otherwise `CODING_AGENT=CC` uses Claude Code.
28
30
 
29
31
  ## Run Wile
30
32
 
@@ -2,7 +2,16 @@
2
2
  # Copy this file to .env and fill in your values
3
3
 
4
4
  # =============================================================================
5
- # REQUIRED - Claude Code Authentication (choose ONE)
5
+ # REQUIRED - Coding Agent Selection
6
+ # =============================================================================
7
+
8
+ # Choose which coding agent to run:
9
+ # - CC = Claude Code (default)
10
+ # - OC = OpenCode (OpenRouter)
11
+ CODING_AGENT=CC
12
+
13
+ # =============================================================================
14
+ # REQUIRED - Claude Code Authentication (CC only, choose ONE)
6
15
  # =============================================================================
7
16
 
8
17
  # OPTION 1: OAuth Token (RECOMMENDED - uses your Pro/Max subscription)
@@ -15,6 +24,15 @@ CC_CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxx
15
24
  # Only use this if you don't have a Pro/Max subscription
16
25
  # CC_ANTHROPIC_API_KEY=sk-ant-xxxxx
17
26
 
27
+ # =============================================================================
28
+ # REQUIRED - OpenCode Authentication (OC only)
29
+ # =============================================================================
30
+
31
+ # OpenRouter API key (used with OpenCode)
32
+ # OC_OPENROUTER_API_KEY=sk-or-xxxxx
33
+ # OpenRouter model id (Wile maps `glm-4.7` to `openrouter/z-ai/glm-4.7`)
34
+ # OC_MODEL=glm-4.7
35
+
18
36
  # =============================================================================
19
37
  # GitHub Authentication (required when WILE_REPO_SOURCE=github)
20
38
  # =============================================================================
@@ -48,6 +48,10 @@ ENV NODE_OPTIONS="--max-old-space-size=4096"
48
48
  # https://www.npmjs.com/package/@anthropic-ai/claude-code
49
49
  RUN npm install -g @anthropic-ai/claude-code
50
50
 
51
+ # Install OpenCode CLI
52
+ # https://www.npmjs.com/package/opencode-ai
53
+ RUN npm install -g opencode-ai@latest
54
+
51
55
  # CC_ANTHROPIC_API_KEY is passed at runtime via environment variable (mapped to ANTHROPIC_API_KEY in entrypoint)
52
56
  # Usage: claude --dangerously-skip-permissions
53
57
 
@@ -110,8 +110,11 @@ For UI work, tell the agent how to verify:
110
110
 
111
111
  | Variable | Required | Description |
112
112
  |----------|----------|-------------|
113
+ | `CODING_AGENT` | No | `CC` (Claude Code, default) or `OC` (OpenCode via OpenRouter) |
113
114
  | `CC_CLAUDE_CODE_OAUTH_TOKEN` | Yes* | OAuth token from `claude setup-token` (uses Pro/Max subscription) |
114
115
  | `CC_ANTHROPIC_API_KEY` | Yes* | API key (uses API credits - alternative to OAuth) |
116
+ | `OC_OPENROUTER_API_KEY` | Yes (OC) | OpenRouter API key for OpenCode |
117
+ | `OC_MODEL` | Yes (OC) | OpenRouter model id (set `glm-4.7` to target `openrouter/z-ai/glm-4.7`) |
115
118
  | `WILE_REPO_SOURCE` | No | `github` (default) or `local` |
116
119
  | `GITHUB_TOKEN` | Yes (github) | GitHub PAT with repo access |
117
120
  | `GITHUB_REPO_URL` | Yes (github) | HTTPS URL to repository |
@@ -119,7 +122,7 @@ For UI work, tell the agent how to verify:
119
122
  | `MAX_ITERATIONS` | No | Max loops (default: 25) |
120
123
  | `CC_CLAUDE_MODEL` | No | Claude model alias/name (default: sonnet) |
121
124
 
122
- *Either `CC_CLAUDE_CODE_OAUTH_TOKEN` or `CC_ANTHROPIC_API_KEY` is required, not both.
125
+ *Either `CC_CLAUDE_CODE_OAUTH_TOKEN` or `CC_ANTHROPIC_API_KEY` is required when `CODING_AGENT=CC`.
123
126
 
124
127
  ## Output Files
125
128
 
@@ -7,7 +7,7 @@
7
7
  set -e
8
8
 
9
9
  echo "══════════════════════════════════════════════════════"
10
- echo " 🗡️ WILE - Container Startup"
10
+ echo " 🌵 WILE - Container Startup"
11
11
  echo "══════════════════════════════════════════════════════"
12
12
 
13
13
  if [ "${WILE_TEST:-}" = "true" ]; then
@@ -67,10 +67,12 @@ NODE
67
67
  exit 0
68
68
  fi
69
69
 
70
+ CODING_AGENT="${CODING_AGENT:-CC}"
70
71
  REPO_SOURCE="${WILE_REPO_SOURCE:-github}"
71
72
  LOCAL_REPO_PATH="${WILE_LOCAL_REPO_PATH:-/home/wile/workspace/repo}"
72
73
  ADDITIONAL_INSTRUCTIONS_PATH="${WILE_ADDITIONAL_INSTRUCTIONS:-}"
73
74
 
75
+ echo " Agent: $CODING_AGENT"
74
76
  if [ "$REPO_SOURCE" = "local" ]; then
75
77
  if [ ! -d "$LOCAL_REPO_PATH" ]; then
76
78
  echo "ERROR: WILE_LOCAL_REPO_PATH does not exist: $LOCAL_REPO_PATH"
@@ -83,52 +85,74 @@ else
83
85
  : "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
84
86
  fi
85
87
 
86
- # Authentication: Either CC_CLAUDE_CODE_OAUTH_TOKEN (Pro/Max subscription) or CC_ANTHROPIC_API_KEY (API credits)
87
- if [ -z "$CC_CLAUDE_CODE_OAUTH_TOKEN" ] && [ -z "$CC_ANTHROPIC_API_KEY" ]; then
88
- echo "ERROR: Either CC_CLAUDE_CODE_OAUTH_TOKEN or CC_ANTHROPIC_API_KEY is required"
89
- echo ""
90
- echo " CC_CLAUDE_CODE_OAUTH_TOKEN - Uses your Pro/Max subscription (recommended)"
91
- echo " CC_ANTHROPIC_API_KEY - Uses API credits (pay per token)"
92
- echo ""
93
- echo "Run 'claude setup-token' on your local machine to get an OAuth token."
94
- exit 1
88
+ # Authentication for selected coding agent
89
+ if [ "$CODING_AGENT" = "OC" ]; then
90
+ if [ -z "$OC_OPENROUTER_API_KEY" ]; then
91
+ echo "ERROR: OC_OPENROUTER_API_KEY is required for OpenCode"
92
+ exit 1
93
+ fi
94
+ if [ -z "$OC_MODEL" ]; then
95
+ echo "ERROR: OC_MODEL is required for OpenCode"
96
+ exit 1
97
+ fi
98
+ else
99
+ if [ -z "$CC_CLAUDE_CODE_OAUTH_TOKEN" ] && [ -z "$CC_ANTHROPIC_API_KEY" ]; then
100
+ echo "ERROR: Either CC_CLAUDE_CODE_OAUTH_TOKEN or CC_ANTHROPIC_API_KEY is required"
101
+ echo ""
102
+ echo " CC_CLAUDE_CODE_OAUTH_TOKEN - Uses your Pro/Max subscription (recommended)"
103
+ echo " CC_ANTHROPIC_API_KEY - Uses API credits (pay per token)"
104
+ echo ""
105
+ echo "Run 'claude setup-token' on your local machine to get an OAuth token."
106
+ exit 1
107
+ fi
95
108
  fi
96
109
 
97
110
  MAX_ITERATIONS=${MAX_ITERATIONS:-25}
98
111
  SCRIPT_DIR="/home/wile/scripts"
99
112
  WORKSPACE="/home/wile/workspace"
100
113
 
101
- if [ "${WILE_MOCK_CLAUDE:-}" = "true" ]; then
114
+ if [ "${WILE_MOCK_CLAUDE:-}" = "true" ] && [ "$CODING_AGENT" = "CC" ]; then
102
115
  echo " Claude: Mocked"
103
116
  MOCK_BIN="/home/wile/mock-bin"
104
117
  mkdir -p "$MOCK_BIN"
105
- cat > "$MOCK_BIN/claude" << 'MOCK'
106
- #!/bin/sh
107
- echo "ANSWER: 2"
108
- echo "<promise>COMPLETE</promise>"
109
- MOCK
118
+ cp "$SCRIPT_DIR/mock-claude.sh" "$MOCK_BIN/claude"
110
119
  chmod +x "$MOCK_BIN/claude"
111
120
  export PATH="$MOCK_BIN:$PATH"
112
121
  fi
113
122
 
114
- # Set up Claude Code authentication
115
- if [ -n "$CC_CLAUDE_CODE_OAUTH_TOKEN" ]; then
116
- echo " Auth: OAuth (Pro/Max subscription)"
123
+ if [ "$CODING_AGENT" = "OC" ]; then
124
+ echo " Auth: OpenRouter (OpenCode)"
125
+ OPENCODE_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/opencode"
126
+ mkdir -p "$OPENCODE_DATA_DIR"
127
+ cat > "$OPENCODE_DATA_DIR/auth.json" << OPENCODEAUTH
128
+ {
129
+ "openrouter": {
130
+ "type": "api",
131
+ "key": "$OC_OPENROUTER_API_KEY"
132
+ }
133
+ }
134
+ OPENCODEAUTH
135
+ chmod 600 "$OPENCODE_DATA_DIR/auth.json"
136
+ export OPENROUTER_API_KEY="$OC_OPENROUTER_API_KEY"
137
+ else
138
+ # Set up Claude Code authentication
139
+ if [ -n "$CC_CLAUDE_CODE_OAUTH_TOKEN" ]; then
140
+ echo " Auth: OAuth (Pro/Max subscription)"
117
141
 
118
- # Create required directories
119
- mkdir -p ~/.claude ~/.config/claude
142
+ # Create required directories
143
+ mkdir -p ~/.claude ~/.config/claude
120
144
 
121
- # Create ~/.claude.json (THE CRITICAL FILE!)
122
- # Without this, Claude Code thinks it's a fresh install and breaks
123
- cat > ~/.claude.json << 'CLAUDEJSON'
145
+ # Create ~/.claude.json (THE CRITICAL FILE!)
146
+ # Without this, Claude Code thinks it's a fresh install and breaks
147
+ cat > ~/.claude.json << 'CLAUDEJSON'
124
148
  {
125
149
  "hasCompletedOnboarding": true,
126
150
  "theme": "dark"
127
151
  }
128
152
  CLAUDEJSON
129
153
 
130
- # Create credentials file with the OAuth token
131
- cat > ~/.claude/.credentials.json << CREDSJSON
154
+ # Create credentials file with the OAuth token
155
+ cat > ~/.claude/.credentials.json << CREDSJSON
132
156
  {
133
157
  "claudeAiOauth": {
134
158
  "accessToken": "$CC_CLAUDE_CODE_OAUTH_TOKEN",
@@ -139,14 +163,15 @@ CLAUDEJSON
139
163
  }
140
164
  CREDSJSON
141
165
 
142
- # Copy to alternate location too
143
- cp ~/.claude/.credentials.json ~/.config/claude/.credentials.json
166
+ # Copy to alternate location too
167
+ cp ~/.claude/.credentials.json ~/.config/claude/.credentials.json
144
168
 
145
- # Ensure ANTHROPIC_API_KEY is not set (it overrides OAuth)
146
- unset ANTHROPIC_API_KEY
147
- else
148
- echo " Auth: API Key (credits)"
149
- export ANTHROPIC_API_KEY="$CC_ANTHROPIC_API_KEY"
169
+ # Ensure ANTHROPIC_API_KEY is not set (it overrides OAuth)
170
+ unset ANTHROPIC_API_KEY
171
+ else
172
+ echo " Auth: API Key (credits)"
173
+ export ANTHROPIC_API_KEY="$CC_ANTHROPIC_API_KEY"
174
+ fi
150
175
  fi
151
176
 
152
177
  if [ "$REPO_SOURCE" = "local" ]; then
@@ -161,7 +186,7 @@ echo ""
161
186
 
162
187
  # Configure git
163
188
  echo "Configuring git..."
164
- git config --global user.name "Wile Bot"
189
+ git config --global user.name "wile"
165
190
  git config --global user.email "wile@bot.local"
166
191
  git config --global credential.helper store
167
192
 
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ const readline = require("node:readline");
3
+
4
+ const rl = readline.createInterface({
5
+ input: process.stdin,
6
+ crlfDelay: Infinity
7
+ });
8
+
9
+ const writeLine = (value) => {
10
+ process.stdout.write(`${value}\n`);
11
+ };
12
+
13
+ const extractText = (content) => {
14
+ if (!Array.isArray(content)) return;
15
+ for (const chunk of content) {
16
+ if (!chunk || typeof chunk !== "object") continue;
17
+ if (chunk.type === "text" && typeof chunk.text === "string") {
18
+ process.stdout.write(chunk.text);
19
+ continue;
20
+ }
21
+ if (chunk.type === "thinking" && typeof chunk.thinking === "string") {
22
+ writeLine(`[thinking] ${chunk.thinking}`);
23
+ }
24
+ }
25
+ };
26
+
27
+ const extractToolUse = (content) => {
28
+ if (!Array.isArray(content)) return;
29
+ for (const chunk of content) {
30
+ if (!chunk || typeof chunk !== "object") continue;
31
+ if (chunk.type === "tool_use") {
32
+ const toolName = chunk.name ?? "tool";
33
+ const description = chunk.input?.description ?? "";
34
+ const command = chunk.input?.command ?? "";
35
+ if (description && command) {
36
+ writeLine(`[tool] ${toolName}: ${description} (${command})`);
37
+ } else if (command) {
38
+ writeLine(`[tool] ${toolName}: ${command}`);
39
+ } else if (description) {
40
+ writeLine(`[tool] ${toolName}: ${description}`);
41
+ } else {
42
+ writeLine(`[tool] ${toolName}`);
43
+ }
44
+ }
45
+ }
46
+ };
47
+
48
+ rl.on("line", (line) => {
49
+ if (!line.trim()) return;
50
+ if (process.env.WILE_STREAM_JSON === "true") {
51
+ writeLine(line);
52
+ return;
53
+ }
54
+ let payload;
55
+ try {
56
+ payload = JSON.parse(line);
57
+ } catch {
58
+ return;
59
+ }
60
+
61
+ if (payload.type === "system" && payload.subtype === "init") {
62
+ const model = payload.model ?? "unknown";
63
+ writeLine(`[system] model: ${model}`);
64
+ return;
65
+ }
66
+
67
+ if (payload.type === "assistant" && payload.message) {
68
+ extractToolUse(payload.message.content);
69
+ extractText(payload.message.content);
70
+ return;
71
+ }
72
+
73
+ if (payload.type === "user" && payload.message?.content) {
74
+ for (const chunk of payload.message.content) {
75
+ if (chunk?.type === "tool_result") {
76
+ writeLine(`[tool-result] ${chunk.tool_use_id ?? "unknown"} ${chunk.is_error ? "error" : "ok"}`);
77
+ }
78
+ }
79
+ return;
80
+ }
81
+
82
+ if (payload.type === "result" && payload.result) {
83
+ writeLine(payload.result);
84
+ }
85
+ });
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ COUNT_FILE="/tmp/wile-claude-mock-count"
5
+ if [ ! -f "$COUNT_FILE" ]; then
6
+ echo "0" > "$COUNT_FILE"
7
+ fi
8
+
9
+ COUNT=$(cat "$COUNT_FILE")
10
+ NEXT_COUNT=$((COUNT + 1))
11
+ echo "$NEXT_COUNT" > "$COUNT_FILE"
12
+
13
+ if [ "$COUNT" -eq 0 ]; then
14
+ cat <<'JSON'
15
+ {"type":"assistant","message":{"content":[{"type":"text","text":"ANSWER: 2\n"}]}}
16
+ JSON
17
+ else
18
+ cat <<'JSON'
19
+ {"type":"assistant","message":{"content":[{"type":"text","text":"<promise>COMPLETE</promise>\n"}]}}
20
+ JSON
21
+ fi
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ const readline = require("node:readline");
3
+
4
+ const rl = readline.createInterface({
5
+ input: process.stdin,
6
+ crlfDelay: Infinity
7
+ });
8
+
9
+ const writeLine = (value) => {
10
+ process.stdout.write(`${value}\n`);
11
+ };
12
+
13
+ const formatToolLine = (tool, title, input) => {
14
+ if (title) return `[tool] ${tool}: ${title}`;
15
+ if (input && Object.keys(input).length > 0) {
16
+ return `[tool] ${tool}: ${JSON.stringify(input)}`;
17
+ }
18
+ return `[tool] ${tool}`;
19
+ };
20
+
21
+ rl.on("line", (line) => {
22
+ if (!line.trim()) return;
23
+ let payload;
24
+ try {
25
+ payload = JSON.parse(line);
26
+ } catch {
27
+ return;
28
+ }
29
+
30
+ if (payload.type === "text" && payload.part?.text) {
31
+ process.stdout.write(payload.part.text);
32
+ return;
33
+ }
34
+
35
+ if (payload.type === "tool_use" && payload.part) {
36
+ const tool = payload.part.tool ?? "tool";
37
+ const title = payload.part.state?.title ?? "";
38
+ const input = payload.part.state?.input ?? {};
39
+ writeLine(formatToolLine(tool, title, input));
40
+ return;
41
+ }
42
+
43
+ if (payload.type === "error" && payload.error) {
44
+ const name = payload.error?.name ?? "error";
45
+ const message = payload.error?.data?.message ?? payload.error?.message ?? "";
46
+ writeLine(`[error] ${name}${message ? `: ${message}` : ""}`);
47
+ return;
48
+ }
49
+ });
@@ -87,6 +87,7 @@ After completing steps 1-8, check if ALL stories in `.wile/prd.json` have `passe
87
87
  ```
88
88
  <promise>COMPLETE</promise>
89
89
  ```
90
+ The entire response must be exactly that single line. No other text before or after. No extra lines. No markdown. No backticks. No code blocks.
90
91
 
91
92
  **If there are still stories with `passes: false`**, end your response normally. The loop will call you again for the next story.
92
93
 
@@ -17,6 +17,7 @@ run_case() {
17
17
  mkdir -p "$SCRIPT_DIR" "$BIN_DIR"
18
18
 
19
19
  cp /Users/thiagoduarte/Projects/personal/wile/packages/agent/scripts/wile.sh "$SCRIPT_DIR/wile.sh"
20
+ cp /Users/thiagoduarte/Projects/personal/wile/packages/agent/scripts/claude-stream.js "$SCRIPT_DIR/claude-stream.js"
20
21
  chmod +x "$SCRIPT_DIR/wile.sh"
21
22
 
22
23
  echo "BASE PROMPT" > "$SCRIPT_DIR/prompt.md"
@@ -43,10 +44,10 @@ EOF
43
44
 
44
45
  CAPTURE="$TMP_DIR/capture.txt"
45
46
 
46
- cat > "$BIN_DIR/claude" <<'EOF'
47
+ cat > "$BIN_DIR/claude" <<'EOF'
47
48
  #!/bin/sh
48
49
  cat > "$CLAUDE_CAPTURE"
49
- echo "<promise>COMPLETE</promise>"
50
+ printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"<promise>COMPLETE</promise>\n"}]}}'
50
51
  EOF
51
52
  chmod +x "$BIN_DIR/claude"
52
53
 
@@ -0,0 +1,44 @@
1
+ #!/bin/sh
2
+ set -euo pipefail
3
+
4
+ TMP_DIR=$(mktemp -d /tmp/wile-iteration-limit-XXXXXX)
5
+ cleanup() {
6
+ rm -rf "$TMP_DIR"
7
+ }
8
+ trap cleanup EXIT INT TERM
9
+
10
+ SCRIPT_DIR="$TMP_DIR/agent"
11
+ BIN_DIR="$TMP_DIR/bin"
12
+ mkdir -p "$SCRIPT_DIR" "$BIN_DIR"
13
+
14
+ cp /Users/thiagoduarte/Projects/personal/wile/packages/agent/scripts/wile.sh "$SCRIPT_DIR/wile.sh"
15
+ cp /Users/thiagoduarte/Projects/personal/wile/packages/agent/scripts/claude-stream.js "$SCRIPT_DIR/claude-stream.js"
16
+ chmod +x "$SCRIPT_DIR/wile.sh"
17
+
18
+ echo "BASE PROMPT" > "$SCRIPT_DIR/prompt.md"
19
+
20
+ OUTPUT_FILE="$TMP_DIR/output.txt"
21
+
22
+ cat > "$BIN_DIR/claude" <<'EOF'
23
+ #!/bin/sh
24
+ printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"text","text":"working...\n"}]}}'
25
+ EOF
26
+ chmod +x "$BIN_DIR/claude"
27
+
28
+ set +e
29
+ PATH="$BIN_DIR:$PATH" \
30
+ CC_CLAUDE_MODEL="sonnet" \
31
+ "$SCRIPT_DIR/wile.sh" 3 > "$OUTPUT_FILE" 2>&1
32
+ EXIT_CODE=$?
33
+ set -e
34
+
35
+ if [ "$EXIT_CODE" -eq 0 ]; then
36
+ echo "error: expected non-zero exit when max iterations reached" >&2
37
+ exit 1
38
+ fi
39
+
40
+ grep -q "MAX ITERATIONS REACHED (3)" "$OUTPUT_FILE"
41
+ grep -q "Iteration 1 of 3" "$OUTPUT_FILE"
42
+ grep -q "Iteration 3 of 3" "$OUTPUT_FILE"
43
+
44
+ echo "test-iteration-limit: ok"
@@ -7,17 +7,31 @@
7
7
  set -e
8
8
 
9
9
  MAX_ITERATIONS=${1:-25}
10
+ CODING_AGENT=${CODING_AGENT:-CC}
10
11
  CLAUDE_MODEL=${CC_CLAUDE_MODEL:-sonnet}
12
+ OC_MODEL=${OC_MODEL:-glm-4.7}
13
+ if [[ "$OC_MODEL" != */* ]]; then
14
+ OC_MODEL="z-ai/$OC_MODEL"
15
+ fi
11
16
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
17
  PROMPT_FILE="$SCRIPT_DIR/prompt.md"
13
18
  SETUP_PROMPT_FILE="$SCRIPT_DIR/prompt-setup.md"
14
19
  ADDITIONAL_PROMPT_FILE="${WILE_ADDITIONAL_INSTRUCTIONS:-}"
20
+ TEE_TARGET="${WILE_TEE_TARGET:-/dev/stderr}"
21
+ if ! ( : > "$TEE_TARGET" ) 2>/dev/null; then
22
+ TEE_TARGET="/dev/null"
23
+ fi
15
24
 
16
25
  echo "══════════════════════════════════════════════════════"
17
- echo " 🗡️ WILE - Autonomous Coding Agent"
26
+ echo " 🌵 WILE - Autonomous Coding Agent"
18
27
  echo "══════════════════════════════════════════════════════"
28
+ echo " Agent: $CODING_AGENT"
19
29
  echo " Max iterations: $MAX_ITERATIONS"
20
- echo " Model: $CLAUDE_MODEL"
30
+ if [ "$CODING_AGENT" = "OC" ]; then
31
+ echo " Model: $OC_MODEL"
32
+ else
33
+ echo " Model: $CLAUDE_MODEL"
34
+ fi
21
35
  echo " Prompt file: $PROMPT_FILE"
22
36
  echo "══════════════════════════════════════════════════════"
23
37
  echo ""
@@ -39,6 +53,29 @@ if [ -n "$ADDITIONAL_PROMPT_FILE" ] && [ -f "$ADDITIONAL_PROMPT_FILE" ]; then
39
53
  fi
40
54
  fi
41
55
 
56
+ run_claude() {
57
+ local prompt_path="$1"
58
+ cat "$prompt_path" \
59
+ | claude --model "$CLAUDE_MODEL" --print --output-format stream-json --verbose --dangerously-skip-permissions \
60
+ | node "$SCRIPT_DIR/claude-stream.js"
61
+ }
62
+
63
+ run_opencode() {
64
+ local prompt_path="$1"
65
+ cat "$prompt_path" \
66
+ | opencode run --format json --model "openrouter/$OC_MODEL" \
67
+ | node "$SCRIPT_DIR/opencode-stream.js"
68
+ }
69
+
70
+ run_agent() {
71
+ local prompt_path="$1"
72
+ if [ "$CODING_AGENT" = "OC" ]; then
73
+ run_opencode "$prompt_path"
74
+ else
75
+ run_claude "$prompt_path"
76
+ fi
77
+ }
78
+
42
79
  # ════════════════════════════════════════════════════════════
43
80
  # ITERATION 0: Setup
44
81
  # ════════════════════════════════════════════════════════════
@@ -49,7 +86,7 @@ echo "════════════════════════
49
86
  echo ""
50
87
 
51
88
  if [ -f "$SETUP_PROMPT_FILE" ]; then
52
- OUTPUT=$(cat "$SETUP_PROMPT_FILE" | claude --model "$CLAUDE_MODEL" --verbose --dangerously-skip-permissions 2>&1 | tee /dev/stderr) || true
89
+ OUTPUT=$(run_agent "$SETUP_PROMPT_FILE" | tee "$TEE_TARGET") || true
53
90
 
54
91
  # Check if setup failed critically
55
92
  if echo "$OUTPUT" | grep -q "<promise>SETUP_FAILED</promise>"; then
@@ -80,15 +117,22 @@ for i in $(seq 1 $MAX_ITERATIONS); do
80
117
  # Pipe prompt to Claude Code
81
118
  # --dangerously-skip-permissions allows autonomous operation
82
119
  # Capture output while also displaying it (tee to stderr)
83
- OUTPUT=$(cat "$PROMPT_FILE" | claude --model "$CLAUDE_MODEL" --verbose --dangerously-skip-permissions 2>&1 | tee /dev/stderr) || true
120
+ OUTPUT=$(run_agent "$PROMPT_FILE" | tee "$TEE_TARGET") || true
84
121
 
85
- # Check for completion signal
86
- if echo "$OUTPUT" | grep -q "<promise>COMPLETE</promise>"; then
122
+ # Check for completion signal (tag must be on its own line; reject backticks/code fences)
123
+ CLEAN_OUTPUT=$(printf '%s' "$OUTPUT" | tr -d '\r' | sed -e 's/[[:space:]]*$//')
124
+ if printf '%s\n' "$CLEAN_OUTPUT" | grep -q -E '^[[:space:]]*<promise>COMPLETE</promise>[[:space:]]*$'; then
125
+ if printf '%s' "$CLEAN_OUTPUT" | grep -F '```' >/dev/null 2>&1; then
126
+ :
127
+ elif printf '%s' "$CLEAN_OUTPUT" | grep -F '`<promise>COMPLETE</promise>`' >/dev/null 2>&1; then
128
+ :
129
+ else
87
130
  echo ""
88
131
  echo "══════════════════════════════════════════════════════"
89
132
  echo " ✅ ALL TASKS COMPLETE"
90
133
  echo "══════════════════════════════════════════════════════"
91
134
  exit 0
135
+ fi
92
136
  fi
93
137
 
94
138
  echo ""
package/dist/cli.js CHANGED
@@ -7544,6 +7544,7 @@ var prdExample = {
7544
7544
  var tips = {
7545
7545
  oauth: "Tip: run 'claude setup-token' on your machine to generate an OAuth token (uses Pro/Max subscription).",
7546
7546
  apiKey: "Tip: create an Anthropic API key in the console (uses API credits).",
7547
+ openrouter: "Tip: create an OpenRouter API key for OpenCode (used for z-ai/glm-4.7).",
7547
7548
  github: "Tip: use a GitHub Personal Access Token (fine-grained recommended). Create at https://github.com/settings/tokens?type=beta with Contents (read/write) and Metadata (read)."
7548
7549
  };
7549
7550
  var readEnvFile = async (path) => {
@@ -7624,44 +7625,73 @@ var runConfig = async () => {
7624
7625
  const agentsPath = join(wileDir, "AGENTS.md");
7625
7626
  await mkdir(secretsDir, { recursive: true });
7626
7627
  const existingEnv = await readEnvFile(envPath);
7627
- await prompt({
7628
+ const codingAgentResponse = await prompt({
7628
7629
  type: "select",
7629
7630
  name: "codingAgent",
7630
7631
  message: "Select coding agent",
7631
- choices: [{ title: "Claude Code (CC)", value: "CC" }],
7632
- initial: 0
7633
- });
7634
- const authDefault = existingEnv.CC_CLAUDE_CODE_OAUTH_TOKEN ? "oauth" : existingEnv.CC_ANTHROPIC_API_KEY ? "apiKey" : "oauth";
7635
- const authResponse = await prompt({
7636
- type: "select",
7637
- name: "authMethod",
7638
- message: "Claude Code authentication",
7639
7632
  choices: [
7640
- { title: "OAuth token (Pro/Max subscription)", value: "oauth" },
7641
- { title: "API key (Anthropic credits)", value: "apiKey" }
7633
+ { title: "Claude Code (CC)", value: "CC" },
7634
+ { title: "OpenCode (OC)", value: "OC" }
7642
7635
  ],
7643
- initial: authDefault === "apiKey" ? 1 : 0
7644
- });
7645
- const authMethod = authResponse.authMethod;
7646
- console.log("");
7647
- console.log(authMethod === "oauth" ? tips.oauth : tips.apiKey);
7648
- console.log("");
7649
- const authValueResponse = await prompt({
7650
- type: "password",
7651
- name: "authValue",
7652
- message: authMethod === "oauth" ? "Claude Code OAuth token (press enter to keep existing)" : "Anthropic API key (press enter to keep existing)",
7653
- initial: authMethod === "oauth" ? existingEnv.CC_CLAUDE_CODE_OAUTH_TOKEN ?? "" : existingEnv.CC_ANTHROPIC_API_KEY ?? ""
7654
- });
7655
- const defaultModelResponse = await prompt({
7656
- type: "select",
7657
- name: "model",
7658
- message: "Default Claude model",
7659
- choices: [
7660
- { title: "sonnet", value: "sonnet" },
7661
- { title: "opus", value: "opus" }
7662
- ],
7663
- initial: existingEnv.CC_CLAUDE_MODEL === "opus" ? 1 : 0
7636
+ initial: existingEnv.CODING_AGENT === "OC" ? 1 : 0
7664
7637
  });
7638
+ const codingAgent = codingAgentResponse.codingAgent;
7639
+ let authMethod = null;
7640
+ let authValueResponse = {};
7641
+ let defaultModelResponse = {};
7642
+ let ocKeyResponse = {};
7643
+ let ocModelResponse = {};
7644
+ if (codingAgent === "CC") {
7645
+ const authDefault = existingEnv.CC_CLAUDE_CODE_OAUTH_TOKEN ? "oauth" : existingEnv.CC_ANTHROPIC_API_KEY ? "apiKey" : "oauth";
7646
+ const authResponse = await prompt({
7647
+ type: "select",
7648
+ name: "authMethod",
7649
+ message: "Claude Code authentication",
7650
+ choices: [
7651
+ { title: "OAuth token (Pro/Max subscription)", value: "oauth" },
7652
+ { title: "API key (Anthropic credits)", value: "apiKey" }
7653
+ ],
7654
+ initial: authDefault === "apiKey" ? 1 : 0
7655
+ });
7656
+ authMethod = authResponse.authMethod;
7657
+ console.log("");
7658
+ console.log(authMethod === "oauth" ? tips.oauth : tips.apiKey);
7659
+ console.log("");
7660
+ authValueResponse = await prompt({
7661
+ type: "password",
7662
+ name: "authValue",
7663
+ message: authMethod === "oauth" ? "Claude Code OAuth token (press enter to keep existing)" : "Anthropic API key (press enter to keep existing)",
7664
+ initial: authMethod === "oauth" ? existingEnv.CC_CLAUDE_CODE_OAUTH_TOKEN ?? "" : existingEnv.CC_ANTHROPIC_API_KEY ?? ""
7665
+ });
7666
+ defaultModelResponse = await prompt({
7667
+ type: "select",
7668
+ name: "model",
7669
+ message: "Default Claude model",
7670
+ choices: [
7671
+ { title: "sonnet", value: "sonnet" },
7672
+ { title: "opus", value: "opus" },
7673
+ { title: "haiku", value: "haiku" }
7674
+ ],
7675
+ initial: existingEnv.CC_CLAUDE_MODEL === "opus" ? 1 : existingEnv.CC_CLAUDE_MODEL === "haiku" ? 2 : 0
7676
+ });
7677
+ } else {
7678
+ console.log("");
7679
+ console.log(tips.openrouter);
7680
+ console.log("");
7681
+ ocKeyResponse = await prompt({
7682
+ type: "password",
7683
+ name: "ocKey",
7684
+ message: "OpenRouter API key (press enter to keep existing)",
7685
+ initial: existingEnv.OC_OPENROUTER_API_KEY ?? ""
7686
+ });
7687
+ ocModelResponse = await prompt({
7688
+ type: "select",
7689
+ name: "ocModel",
7690
+ message: "OpenCode model (OpenRouter)",
7691
+ choices: [{ title: "glm-4.7", value: "glm-4.7" }],
7692
+ initial: existingEnv.OC_MODEL === "glm-4.7" ? 0 : 0
7693
+ });
7694
+ }
7665
7695
  const repoSourceResponse = await prompt({
7666
7696
  type: "select",
7667
7697
  name: "repoSource",
@@ -7696,23 +7726,39 @@ var runConfig = async () => {
7696
7726
  message: "Default branch name",
7697
7727
  initial: existingEnv.BRANCH_NAME ?? "main"
7698
7728
  });
7729
+ const iterationsResponse = await prompt({
7730
+ type: "number",
7731
+ name: "maxIterations",
7732
+ message: "Default max iterations",
7733
+ initial: existingEnv.WILE_MAX_ITERATIONS ? Number(existingEnv.WILE_MAX_ITERATIONS) : 25
7734
+ });
7735
+ const fallbackIterations = existingEnv.WILE_MAX_ITERATIONS ? Number(existingEnv.WILE_MAX_ITERATIONS) : 25;
7736
+ const maxIterations = Number.isFinite(iterationsResponse.maxIterations) && iterationsResponse.maxIterations > 0 ? iterationsResponse.maxIterations : fallbackIterations;
7699
7737
  const authFallback = authMethod === "oauth" ? existingEnv.CC_CLAUDE_CODE_OAUTH_TOKEN : existingEnv.CC_ANTHROPIC_API_KEY;
7700
- const authValue = coalesceValue(authValueResponse.authValue, authFallback);
7738
+ const authValue = codingAgent === "CC" ? coalesceValue(authValueResponse.authValue, authFallback) : undefined;
7739
+ const ocKey = codingAgent === "OC" ? coalesceValue(ocKeyResponse.ocKey, existingEnv.OC_OPENROUTER_API_KEY) : undefined;
7740
+ const ocModel = codingAgent === "OC" ? coalesceValue(ocModelResponse.ocModel, existingEnv.OC_MODEL ?? "glm-4.7") : undefined;
7701
7741
  const githubToken = repoSource === "github" ? coalesceValue(githubTokenResponse.githubToken, existingEnv.GITHUB_TOKEN) : existingEnv.GITHUB_TOKEN;
7702
7742
  const repoUrl = repoSource === "github" ? coalesceValue(repoResponse.repoUrl, existingEnv.GITHUB_REPO_URL) : existingEnv.GITHUB_REPO_URL;
7703
7743
  const branchName = coalesceValue(branchResponse.branchName, existingEnv.BRANCH_NAME ?? "main");
7704
7744
  const envLines = [
7705
- "CODING_AGENT=CC",
7745
+ `CODING_AGENT=${codingAgent}`,
7706
7746
  `WILE_REPO_SOURCE=${repoSource}`,
7707
7747
  `GITHUB_TOKEN=${githubToken ?? ""}`,
7708
7748
  `GITHUB_REPO_URL=${repoUrl ?? ""}`,
7709
7749
  `BRANCH_NAME=${branchName ?? "main"}`,
7710
- `CC_CLAUDE_MODEL=${defaultModelResponse.model}`
7750
+ `WILE_MAX_ITERATIONS=${maxIterations}`
7711
7751
  ];
7712
- if (authMethod === "oauth") {
7713
- envLines.push(`CC_CLAUDE_CODE_OAUTH_TOKEN=${authValue ?? ""}`);
7752
+ if (codingAgent === "CC") {
7753
+ envLines.push(`CC_CLAUDE_MODEL=${defaultModelResponse.model}`);
7754
+ if (authMethod === "oauth") {
7755
+ envLines.push(`CC_CLAUDE_CODE_OAUTH_TOKEN=${authValue ?? ""}`);
7756
+ } else {
7757
+ envLines.push(`CC_ANTHROPIC_API_KEY=${authValue ?? ""}`);
7758
+ }
7714
7759
  } else {
7715
- envLines.push(`CC_ANTHROPIC_API_KEY=${authValue ?? ""}`);
7760
+ envLines.push(`OC_MODEL=${ocModel ?? "glm-4.7"}`);
7761
+ envLines.push(`OC_OPENROUTER_API_KEY=${ocKey ?? ""}`);
7716
7762
  }
7717
7763
  await writeFile(envPath, envLines.join(`
7718
7764
  `) + `
@@ -7815,13 +7861,19 @@ var readWileConfig = (options = {}) => {
7815
7861
  const envProject = parseEnvFile(paths.envProjectPath);
7816
7862
  const repoSource = env.WILE_REPO_SOURCE || "github";
7817
7863
  if (validate) {
7818
- ensureRequired(env.CODING_AGENT === "CC", "CODING_AGENT must be set to CC in .wile/secrets/.env. Run 'bunx wile config'.");
7864
+ ensureRequired(env.CODING_AGENT === "CC" || env.CODING_AGENT === "OC", "CODING_AGENT must be set to CC or OC in .wile/secrets/.env. Run 'bunx wile config'.");
7819
7865
  if (repoSource === "github") {
7820
7866
  ensureRequired(Boolean(env.GITHUB_TOKEN), "GITHUB_TOKEN is required in .wile/secrets/.env. Run 'bunx wile config'.");
7821
7867
  ensureRequired(Boolean(env.GITHUB_REPO_URL), "GITHUB_REPO_URL is required in .wile/secrets/.env. Run 'bunx wile config'.");
7822
7868
  ensureRequired(Boolean(env.BRANCH_NAME), "BRANCH_NAME is required in .wile/secrets/.env. Run 'bunx wile config'.");
7823
7869
  }
7824
- ensureRequired(Boolean(env.CC_CLAUDE_CODE_OAUTH_TOKEN || env.CC_ANTHROPIC_API_KEY), "Either CC_CLAUDE_CODE_OAUTH_TOKEN or CC_ANTHROPIC_API_KEY is required in .wile/secrets/.env.");
7870
+ if (env.CODING_AGENT === "CC") {
7871
+ ensureRequired(Boolean(env.CC_CLAUDE_CODE_OAUTH_TOKEN || env.CC_ANTHROPIC_API_KEY), "Either CC_CLAUDE_CODE_OAUTH_TOKEN or CC_ANTHROPIC_API_KEY is required in .wile/secrets/.env.");
7872
+ }
7873
+ if (env.CODING_AGENT === "OC") {
7874
+ ensureRequired(Boolean(env.OC_OPENROUTER_API_KEY), "OC_OPENROUTER_API_KEY is required in .wile/secrets/.env for OpenCode.");
7875
+ ensureRequired(Boolean(env.OC_MODEL), "OC_MODEL is required in .wile/secrets/.env for OpenCode.");
7876
+ }
7825
7877
  }
7826
7878
  return {
7827
7879
  paths,
@@ -7832,8 +7884,11 @@ var readWileConfig = (options = {}) => {
7832
7884
  branchName: env.BRANCH_NAME ?? "",
7833
7885
  repoSource,
7834
7886
  ccClaudeModel: env.CC_CLAUDE_MODEL,
7887
+ maxIterations: env.WILE_MAX_ITERATIONS,
7835
7888
  ccClaudeCodeOauthToken: env.CC_CLAUDE_CODE_OAUTH_TOKEN,
7836
7889
  ccAnthropicApiKey: env.CC_ANTHROPIC_API_KEY,
7890
+ ocOpenrouterApiKey: env.OC_OPENROUTER_API_KEY,
7891
+ ocModel: env.OC_MODEL,
7837
7892
  envProject
7838
7893
  }
7839
7894
  };
@@ -7998,6 +8053,7 @@ var runWile = async (options) => {
7998
8053
  console.log(`- cwd: ${cwd}`);
7999
8054
  console.log(`- INIT_CWD: ${initCwd}`);
8000
8055
  console.log(`- WILE_AGENT_DIR: ${process.env.WILE_AGENT_DIR ?? "(unset)"}`);
8056
+ console.log(`- codingAgent: ${config.codingAgent}`);
8001
8057
  console.log(`- repoSource: ${config.repoSource}`);
8002
8058
  console.log(`- githubRepoUrl: ${config.githubRepoUrl || "(empty)"}`);
8003
8059
  console.log(`- branchName: ${config.branchName || "(empty)"}`);
@@ -8018,8 +8074,9 @@ var runWile = async (options) => {
8018
8074
  return;
8019
8075
  }
8020
8076
  const agentDir = resolveAgentDir();
8077
+ const resolvedIterations = options.maxIterations || config.maxIterations || "25";
8021
8078
  buildAgentImage(agentDir);
8022
- const dockerArgs = buildDockerArgs(options, config, paths, cwd);
8079
+ const dockerArgs = buildDockerArgs({ ...options, maxIterations: resolvedIterations }, config, paths, cwd);
8023
8080
  const logsDir = join3(paths.wileDir, "logs");
8024
8081
  mkdirSync(logsDir, { recursive: true });
8025
8082
  const logPath = join3(logsDir, `run-${getTimestamp()}.log`);
@@ -8049,7 +8106,7 @@ program2.name("wile").description("Autonomous AI coding agent that ships feature
8049
8106
  program2.command("config").description("Configure the current project for Wile").action(async () => {
8050
8107
  await runConfig();
8051
8108
  });
8052
- program2.command("run").description("Run Wile on a repository").option("--repo <repo>", "Repository URL or local path").option("--max-iterations <count>", "Maximum iterations", "25").option("--test", "Run in test mode").option("--debug", "Print debug info before running").action(async (options) => {
8109
+ program2.command("run").description("Run Wile on a repository").option("--repo <repo>", "Repository URL or local path").option("--max-iterations <count>", "Maximum iterations").option("--test", "Run in test mode").option("--debug", "Print debug info before running").action(async (options) => {
8053
8110
  await runWile(options);
8054
8111
  });
8055
8112
  program2.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wile",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Autonomous AI coding agent that ships features while you sleep",
5
5
  "type": "module",
6
6
  "bin": {