wile 0.6.9 → 1.0.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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/.env.example +17 -0
  3. package/dist/agent/Dockerfile +9 -7
  4. package/dist/agent/README.md +26 -22
  5. package/dist/agent/entrypoint.sh +76 -21
  6. package/dist/agent/scripts/claude-stream.ts +159 -0
  7. package/dist/agent/scripts/codex-stream.ts +167 -0
  8. package/dist/agent/scripts/gemini-stream.ts +97 -0
  9. package/dist/agent/scripts/mock-claude.sh +48 -21
  10. package/dist/agent/scripts/mock-codex.sh +126 -0
  11. package/dist/agent/scripts/mock-gemini.sh +48 -21
  12. package/dist/agent/scripts/opencode-stream.ts +93 -0
  13. package/dist/agent/scripts/prompt-compact.md +36 -34
  14. package/dist/agent/scripts/prompt-preflight.md +0 -7
  15. package/dist/agent/scripts/prompt.md +26 -24
  16. package/dist/agent/scripts/test-additional-instructions.sh +4 -2
  17. package/dist/agent/scripts/test-env-project-docker.sh +5 -4
  18. package/dist/agent/scripts/test-iteration-limit.sh +4 -2
  19. package/dist/agent/scripts/test-prd-validation-docker.sh +151 -0
  20. package/dist/agent/scripts/test-preflight-claude-docker.sh +5 -4
  21. package/dist/agent/scripts/test-preflight-docker.sh +16 -13
  22. package/dist/agent/scripts/validate-compact.ts +134 -0
  23. package/dist/agent/scripts/validate-prd.ts +280 -0
  24. package/dist/agent/scripts/wile-compact.sh +21 -5
  25. package/dist/agent/scripts/wile-preflight.sh +19 -3
  26. package/dist/agent/scripts/wile.sh +19 -3
  27. package/dist/cli.js +322 -65
  28. package/package.json +1 -1
  29. package/dist/agent/scripts/claude-stream.js +0 -88
  30. package/dist/agent/scripts/gemini-stream.js +0 -63
  31. package/dist/agent/scripts/opencode-stream.js +0 -52
  32. package/dist/agent/scripts/validate-compact.js +0 -135
package/README.md CHANGED
@@ -26,7 +26,7 @@ This creates:
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
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), `CODING_AGENT=GC` to use Gemini CLI, otherwise `CODING_AGENT=CC` uses Claude Code.
29
+ Set `CODING_AGENT=CX` to use Codex CLI, `CODING_AGENT=OC` to use OpenCode (OpenRouter), `CODING_AGENT=GC` to use Gemini CLI, otherwise `CODING_AGENT=CC` uses Claude Code.
30
30
 
31
31
  ## Run Wile
32
32
 
@@ -9,6 +9,7 @@
9
9
  # - CC = Claude Code (default)
10
10
  # - GC = Gemini CLI (Google account OAuth)
11
11
  # - OC = OpenCode (OpenRouter)
12
+ # - CX = Codex CLI (ChatGPT subscription)
12
13
  CODING_AGENT=CC
13
14
 
14
15
  # =============================================================================
@@ -47,6 +48,19 @@ CC_CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxx
47
48
  # Get one at: https://aistudio.google.com/app/apikey
48
49
  # GEMINI_API_KEY=your-key
49
50
 
51
+ # =============================================================================
52
+ # REQUIRED - Codex CLI Authentication (CX only, choose ONE)
53
+ # =============================================================================
54
+
55
+ # OPTION 1: ChatGPT login (RECOMMENDED - uses your ChatGPT subscription)
56
+ # Run: codex login (or codex login --device-auth)
57
+ # Then base64 encode ~/.codex/auth.json and paste here
58
+ # CODEX_AUTH_JSON_B64=eyJ...
59
+
60
+ # OPTION 2: API key (uses API credits - pay per token)
61
+ # Get one at: https://platform.openai.com/api-keys
62
+ # CODEX_API_KEY=sk-openai-xxxxx
63
+
50
64
  # =============================================================================
51
65
  # GitHub Authentication (required when WILE_REPO_SOURCE=github)
52
66
  # =============================================================================
@@ -83,6 +97,9 @@ CC_CLAUDE_MODEL=sonnet
83
97
  # Gemini model name (default: auto-gemini-3)
84
98
  # GEMINI_MODEL=auto-gemini-3
85
99
 
100
+ # Codex model name (defaults to Codex CLI default)
101
+ # CODEX_MODEL=gpt-5.1-codex
102
+
86
103
  # =============================================================================
87
104
  # PROJECT ENVIRONMENT VARIABLES
88
105
  # =============================================================================
@@ -34,7 +34,7 @@ JSON
34
34
  RUN cd /opt/agent-browser \
35
35
  && bun add agent-browser \
36
36
  && node /opt/agent-browser/node_modules/agent-browser/scripts/postinstall.js \
37
- && ln -s /opt/agent-browser/node_modules/agent-browser/bin/agent-browser /usr/local/bin/agent-browser
37
+ && ln -s /opt/agent-browser/node_modules/agent-browser/bin/agent-browser.js /usr/local/bin/agent-browser
38
38
 
39
39
  # Create non-root user
40
40
  RUN useradd -m -s /bin/bash wile \
@@ -63,11 +63,14 @@ RUN npm install -g opencode-ai@latest
63
63
  # https://www.npmjs.com/package/@google/gemini-cli
64
64
  RUN npm install -g @google/gemini-cli
65
65
 
66
+ # Install Codex CLI
67
+ # https://www.npmjs.com/package/@openai/codex
68
+ RUN npm install -g @openai/codex
69
+
66
70
  # CC_ANTHROPIC_API_KEY is passed at runtime via environment variable (mapped to ANTHROPIC_API_KEY in entrypoint)
67
71
  # Usage: claude --dangerously-skip-permissions
68
72
 
69
- # Install Playwright with Chromium for browser testing
70
- # Dependencies for headless Chromium
73
+ # Install Chromium dependencies for Playwright
71
74
  RUN apt-get update && apt-get install -y \
72
75
  libnss3 \
73
76
  libnspr4 \
@@ -87,11 +90,10 @@ RUN apt-get update && apt-get install -y \
87
90
  libatspi2.0-0 \
88
91
  && rm -rf /var/lib/apt/lists/*
89
92
 
90
- # Install Playwright globally and download Chromium to shared location
93
+ # Download Chromium using the Playwright core version bundled with agent-browser
91
94
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
92
- RUN npm install -g playwright \
93
- && mkdir -p /opt/playwright-browsers \
94
- && npx playwright install chromium \
95
+ RUN mkdir -p /opt/playwright-browsers \
96
+ && node /opt/agent-browser/node_modules/playwright-core/cli.js install chromium \
95
97
  && chmod -R 755 /opt/playwright-browsers
96
98
 
97
99
  # Copy scripts
@@ -22,24 +22,24 @@ docker compose up --build
22
22
  1. Container clones your repository
23
23
  2. Checks out the specified branch
24
24
  3. **Iteration 0 (Setup)**: Sets up .gitignore, initializes progress.txt
25
- 4. Reads `.wile/prd.json` from repo
26
- 5. Loops through user stories, implementing one per iteration:
27
- - Picks highest priority story where `passes: false`
25
+ 4. Reads `.wile/prd.json`
26
+ 5. Loops through stories, implementing one per iteration:
27
+ - Picks first runnable story in array order where `status: "pending"` and dependencies are done
28
28
  - Implements the feature/fix
29
29
  - Runs tests and typecheck
30
- - Commits and pushes
31
- - Marks story as complete
30
+ - Marks story as done
32
31
  - Logs learnings
33
- 6. Repeats until all stories pass or max iterations reached
32
+ - Commits changes
33
+ 6. Repeats until all stories are done or max iterations reached
34
34
 
35
35
  ## Project Structure
36
36
 
37
- Your repository must have a `.wile/` folder:
37
+ Your repository must have a `.wile/prd.json` file:
38
38
 
39
39
  ```
40
40
  your-repo/
41
41
  ├── .wile/
42
- │ ├── prd.json # User stories (required)
42
+ │ ├── prd.json # Stories backlog (required)
43
43
  │ ├── progress.txt # Learnings log (created by agent)
44
44
  │ └── screenshots/ # Visual verification (created by agent)
45
45
  └── ... your code ...
@@ -49,18 +49,18 @@ your-repo/
49
49
 
50
50
  ```json
51
51
  {
52
- "userStories": [
52
+ "stories": [
53
53
  {
54
- "id": "US-001",
54
+ "id": 1,
55
55
  "title": "Add login form",
56
+ "description": "Email/password form with validation",
56
57
  "acceptanceCriteria": [
57
58
  "Email/password fields exist",
58
59
  "Form validates email format",
59
60
  "typecheck passes"
60
61
  ],
61
- "priority": 1,
62
- "passes": false,
63
- "notes": "Optional notes"
62
+ "dependsOn": [],
63
+ "status": "pending"
64
64
  }
65
65
  ]
66
66
  }
@@ -68,12 +68,12 @@ your-repo/
68
68
 
69
69
  ### Fields
70
70
 
71
- - `id`: Unique identifier (used in commit messages)
71
+ - `id`: Unique numeric identifier
72
72
  - `title`: Short description
73
+ - `description`: Additional context for implementation
73
74
  - `acceptanceCriteria`: List of requirements (be specific!)
74
- - `priority`: Lower number = done first
75
- - `passes`: Set to `true` by the agent when complete
76
- - `notes`: Optional context for the agent
75
+ - `dependsOn`: Array of prerequisite story IDs
76
+ - `status`: `pending` or `done`
77
77
 
78
78
  ## Tips for Success
79
79
 
@@ -110,13 +110,15 @@ 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), `GC` (Gemini CLI), or `OC` (OpenCode via OpenRouter) |
113
+ | `CODING_AGENT` | No | `CC` (Claude Code, default), `GC` (Gemini CLI), `OC` (OpenCode via OpenRouter), or `CX` (Codex CLI) |
114
114
  | `CC_CLAUDE_CODE_OAUTH_TOKEN` | Yes* | OAuth token from `claude setup-token` (uses Pro/Max subscription) |
115
115
  | `CC_ANTHROPIC_API_KEY` | Yes* | API key (uses API credits - alternative to OAuth) |
116
116
  | `GEMINI_OAUTH_CREDS_B64` | Yes (GC)* | Base64 OAuth creds from `~/.gemini/oauth_creds.json` (uses Google account) |
117
117
  | `GEMINI_API_KEY` | Yes (GC)* | Gemini API key (uses API credits - alternative to OAuth) |
118
118
  | `OC_OPENROUTER_API_KEY` | Yes (OC) | OpenRouter API key for OpenCode |
119
119
  | `OC_MODEL` | Yes (OC) | OpenRouter model id (set `glm-4.7` to target `openrouter/z-ai/glm-4.7`) |
120
+ | `CODEX_AUTH_JSON_B64` | Yes (CX)* | Base64 of `~/.codex/auth.json` from `codex login` (uses ChatGPT subscription) |
121
+ | `CODEX_API_KEY` | Yes (CX)* | OpenAI API key (uses API credits - alternative to auth.json) |
120
122
  | `GEMINI_MODEL` | No (GC) | Gemini model name (default: `auto-gemini-3`) |
121
123
  | `WILE_REPO_SOURCE` | No | `github` (default) or `local` |
122
124
  | `GITHUB_TOKEN` | Yes (github) | GitHub PAT with repo access |
@@ -124,15 +126,17 @@ For UI work, tell the agent how to verify:
124
126
  | `BRANCH_NAME` | Yes (github) | Branch to work on |
125
127
  | `MAX_ITERATIONS` | No | Max loops (default: 25) |
126
128
  | `CC_CLAUDE_MODEL` | No | Claude model alias/name (default: sonnet) |
129
+ | `CODEX_MODEL` | No (CX) | Codex model name (defaults to Codex CLI default) |
127
130
 
128
131
  *Either `CC_CLAUDE_CODE_OAUTH_TOKEN` or `CC_ANTHROPIC_API_KEY` is required when `CODING_AGENT=CC`.
129
- *Either `GEMINI_OAUTH_CREDS_B64` or `GEMINI_API_KEY` is required when `CODING_AGENT=GC`.
132
+ *Either `GEMINI_OAUTH_CREDS_B64` or `GEMINI_API_KEY` is required when `CODING_AGENT=GC`.
133
+ *Either `CODEX_AUTH_JSON_B64` or `CODEX_API_KEY` is required when `CODING_AGENT=CX`.
130
134
 
131
135
  ## Output Files
132
136
 
133
- The `.wile/` folder is **tracked in git** (except screenshots):
137
+ Tracked artifacts:
134
138
 
135
- - `prd.json` - User stories / Product Requirements Document
139
+ - `.wile/prd.json` - Product requirements stories backlog
136
140
  - `progress.txt` - Log of completed work and learnings
137
141
  - `screenshots/` - Visual verification screenshots (gitignored)
138
142
 
@@ -153,5 +157,5 @@ git show origin/your-branch:.wile/progress.txt
153
157
  Check story status:
154
158
 
155
159
  ```bash
156
- git show origin/your-branch:.wile/prd.json | jq '.userStories[] | {id, title, passes}'
160
+ git show origin/your-branch:.wile/prd.json | jq '.stories[] | {id, title, status}'
157
161
  ```
@@ -15,6 +15,8 @@ if command -v sysctl >/dev/null 2>&1; then
15
15
  sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null 2>&1 || true
16
16
  fi
17
17
 
18
+ SCRIPT_DIR="/home/wile/scripts"
19
+
18
20
  if [ "${WILE_TEST:-}" = "true" ]; then
19
21
  TEST_REPO="${WILE_TEST_REPO_PATH:-/home/wile/workspace/repo}"
20
22
 
@@ -30,18 +32,19 @@ if [ "${WILE_TEST:-}" = "true" ]; then
30
32
  exit 1
31
33
  fi
32
34
 
35
+ bun "$SCRIPT_DIR/validate-prd.ts" --path .wile/prd.json >/dev/null
36
+
33
37
  echo "TEST MODE: running mocked agent (no GitHub, no Claude)"
34
38
 
35
39
  node - << 'NODE'
36
40
  const fs = require('fs');
37
- const path = require('path');
38
-
39
- const prdPath = path.join('.wile', 'prd.json');
40
- const progressPath = path.join('.wile', 'progress.txt');
41
+ const prdPath = '.wile/prd.json';
42
+ const progressPath = '.wile/progress.txt';
41
43
 
42
44
  const prd = JSON.parse(fs.readFileSync(prdPath, 'utf8'));
43
- const stories = Array.isArray(prd.userStories) ? prd.userStories : [];
44
- const pending = stories.filter((story) => story.passes === false);
45
+ const stories = Array.isArray(prd.stories) ? prd.stories : [];
46
+ const storyById = new Map(stories.map((story) => [story.id, story]));
47
+ const pending = stories.filter((story) => story.status === 'pending');
45
48
 
46
49
  if (process.env.TEST_FORWARD) {
47
50
  console.log(`Forwarded env: ${process.env.TEST_FORWARD}`);
@@ -52,14 +55,20 @@ if (pending.length === 0) {
52
55
  process.exit(0);
53
56
  }
54
57
 
55
- pending.sort((a, b) => {
56
- const aPriority = typeof a.priority === 'number' ? a.priority : Number.MAX_SAFE_INTEGER;
57
- const bPriority = typeof b.priority === 'number' ? b.priority : Number.MAX_SAFE_INTEGER;
58
- return aPriority - bPriority;
59
- });
58
+ const story =
59
+ stories.find(
60
+ (candidate) =>
61
+ candidate.status === 'pending' &&
62
+ Array.isArray(candidate.dependsOn) &&
63
+ candidate.dependsOn.every((depId) => storyById.get(depId)?.status === 'done')
64
+ ) || null;
65
+
66
+ if (!story) {
67
+ console.error('No runnable pending stories in test mode.');
68
+ process.exit(1);
69
+ }
60
70
 
61
- const story = pending[0];
62
- story.passes = true;
71
+ story.status = 'done';
63
72
  fs.writeFileSync(prdPath, JSON.stringify(prd, null, 2) + '\n');
64
73
 
65
74
  if (!fs.existsSync(progressPath)) {
@@ -115,6 +124,16 @@ elif [ "$CODING_AGENT" = "GC" ]; then
115
124
  echo "Run 'gemini' locally and choose Login with Google to create ~/.gemini/oauth_creds.json."
116
125
  exit 1
117
126
  fi
127
+ elif [ "$CODING_AGENT" = "CX" ]; then
128
+ if [ -z "$CODEX_AUTH_JSON_B64" ] && [ -z "$CODEX_AUTH_JSON_PATH" ] && [ -z "$CODEX_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
129
+ echo "ERROR: Either CODEX_AUTH_JSON_B64 (or CODEX_AUTH_JSON_PATH) or CODEX_API_KEY is required"
130
+ echo ""
131
+ echo " CODEX_AUTH_JSON_B64 - Base64 of ~/.codex/auth.json from 'codex login' (recommended)"
132
+ echo " CODEX_API_KEY - OpenAI API key (pay per token)"
133
+ echo ""
134
+ echo "Run 'codex login' (or 'codex login --device-auth') locally, then base64 ~/.codex/auth.json."
135
+ exit 1
136
+ fi
118
137
  else
119
138
  if [ -z "$CC_CLAUDE_CODE_OAUTH_TOKEN" ] && [ -z "$CC_ANTHROPIC_API_KEY" ]; then
120
139
  echo "ERROR: Either CC_CLAUDE_CODE_OAUTH_TOKEN or CC_ANTHROPIC_API_KEY is required"
@@ -128,7 +147,6 @@ else
128
147
  fi
129
148
 
130
149
  MAX_ITERATIONS=${MAX_ITERATIONS:-25}
131
- SCRIPT_DIR="/home/wile/scripts"
132
150
  WORKSPACE="/home/wile/workspace"
133
151
 
134
152
  if [ "${WILE_MOCK_CLAUDE:-}" = "true" ] && [ "$CODING_AGENT" = "CC" ]; then
@@ -149,6 +167,15 @@ if [ "${WILE_MOCK_GEMINI:-}" = "true" ] && [ "$CODING_AGENT" = "GC" ]; then
149
167
  export PATH="$MOCK_BIN:$PATH"
150
168
  fi
151
169
 
170
+ if [ "${WILE_MOCK_CODEX:-}" = "true" ] && [ "$CODING_AGENT" = "CX" ]; then
171
+ echo " Codex: Mocked"
172
+ MOCK_BIN="/home/wile/mock-bin"
173
+ mkdir -p "$MOCK_BIN"
174
+ cp "$SCRIPT_DIR/mock-codex.sh" "$MOCK_BIN/codex"
175
+ chmod +x "$MOCK_BIN/codex"
176
+ export PATH="$MOCK_BIN:$PATH"
177
+ fi
178
+
152
179
  if [ "$CODING_AGENT" = "OC" ]; then
153
180
  if [ "$OC_PROVIDER" = "openrouter" ]; then
154
181
  echo " Auth: OpenRouter (OpenCode)"
@@ -212,6 +239,30 @@ JSON
212
239
  }
213
240
  JSON
214
241
  fi
242
+ elif [ "$CODING_AGENT" = "CX" ]; then
243
+ CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
244
+ export CODEX_HOME
245
+
246
+ if [ -n "$CODEX_AUTH_JSON_B64" ] || [ -n "$CODEX_AUTH_JSON_PATH" ]; then
247
+ echo " Auth: ChatGPT (subscription)"
248
+ mkdir -p "$CODEX_HOME"
249
+ if [ -n "$CODEX_AUTH_JSON_B64" ]; then
250
+ echo "$CODEX_AUTH_JSON_B64" | base64 -d > "$CODEX_HOME/auth.json"
251
+ else
252
+ cp "$CODEX_AUTH_JSON_PATH" "$CODEX_HOME/auth.json"
253
+ fi
254
+ else
255
+ echo " Auth: API Key (OpenAI)"
256
+ CODEX_API_KEY_VALUE="${CODEX_API_KEY:-$OPENAI_API_KEY}"
257
+ mkdir -p "$CODEX_HOME"
258
+ cat > "$CODEX_HOME/auth.json" << EOF
259
+ {
260
+ "auth_mode": "apikey",
261
+ "OPENAI_API_KEY": "$CODEX_API_KEY_VALUE"
262
+ }
263
+ EOF
264
+ fi
265
+ chmod 600 "$CODEX_HOME/auth.json"
215
266
  else
216
267
  # Set up Claude Code authentication
217
268
  if [ -n "$CC_CLAUDE_CODE_OAUTH_TOKEN" ]; then
@@ -314,24 +365,28 @@ echo "Checking for .wile/prd.json..."
314
365
  if [ ! -f ".wile/prd.json" ]; then
315
366
  echo "ERROR: .wile/prd.json not found!"
316
367
  echo ""
317
- echo "Your repository must have a .wile/prd.json file at the root."
318
- echo "This file contains the user stories for Wile to implement."
368
+ echo "Your repository must have a .wile/prd.json file."
369
+ echo "This file contains the stories for Wile to implement."
319
370
  echo ""
320
371
  echo "Example structure:"
321
372
  echo ' {'
322
- echo ' "userStories": ['
373
+ echo ' "stories": ['
323
374
  echo ' {'
324
- echo ' "id": "US-001",'
375
+ echo ' "id": 1,'
325
376
  echo ' "title": "My feature",'
377
+ echo ' "description": "What this story delivers",'
326
378
  echo ' "acceptanceCriteria": ["..."],'
327
- echo ' "priority": 1,'
328
- echo ' "passes": false'
379
+ echo ' "dependsOn": [],'
380
+ echo ' "status": "pending"'
329
381
  echo ' }'
330
382
  echo ' ]'
331
383
  echo ' }'
332
384
  exit 1
333
385
  fi
334
386
 
387
+ echo "Validating .wile/prd.json..."
388
+ bun "$SCRIPT_DIR/validate-prd.ts" --path .wile/prd.json
389
+
335
390
  # Set up .wile/screenshots directory
336
391
  echo "Setting up .wile directory..."
337
392
  mkdir -p .wile/screenshots
@@ -390,7 +445,7 @@ elif [ $EXIT_CODE -eq 1 ]; then
390
445
  git commit -m "WIP: wile stopped at max iterations ($TIMESTAMP)
391
446
 
392
447
  Partial work committed. Check .wile/progress.txt for details.
393
- Some stories may still have passes=false in .wile/prd.json."
448
+ Some stories may still have status=\"pending\" in .wile/prd.json."
394
449
  git push
395
450
  fi
396
451
  elif [ $EXIT_CODE -eq 2 ]; then
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bun
2
+ import { createInterface } from "node:readline";
3
+
4
+ type JsonObject = Record<string, unknown>;
5
+
6
+ const isObject = (value: unknown): value is JsonObject =>
7
+ typeof value === "object" && value !== null;
8
+
9
+ const getString = (obj: JsonObject, key: string): string | undefined => {
10
+ const value = obj[key];
11
+ return typeof value === "string" ? value : undefined;
12
+ };
13
+
14
+ const getObject = (obj: JsonObject, key: string): JsonObject | undefined => {
15
+ const value = obj[key];
16
+ return isObject(value) ? value : undefined;
17
+ };
18
+
19
+ const writeLine = (value: string): void => {
20
+ process.stdout.write(`${value}\n`);
21
+ };
22
+
23
+ const writeTextChunk = (text: string): void => {
24
+ process.stdout.write(text);
25
+ if (!text.endsWith("\n")) {
26
+ process.stdout.write("\n");
27
+ }
28
+ };
29
+
30
+ const extractText = (content: unknown): void => {
31
+ if (!Array.isArray(content)) {
32
+ return;
33
+ }
34
+
35
+ for (const rawChunk of content) {
36
+ if (!isObject(rawChunk)) {
37
+ continue;
38
+ }
39
+
40
+ const chunkType = getString(rawChunk, "type");
41
+ if (chunkType === "text") {
42
+ const text = getString(rawChunk, "text");
43
+ if (text) {
44
+ writeTextChunk(text);
45
+ }
46
+ continue;
47
+ }
48
+
49
+ if (chunkType === "thinking") {
50
+ const thinking = getString(rawChunk, "thinking");
51
+ if (thinking) {
52
+ writeLine(`[thinking] ${thinking}`);
53
+ }
54
+ }
55
+ }
56
+ };
57
+
58
+ const extractToolUse = (content: unknown): void => {
59
+ if (!Array.isArray(content)) {
60
+ return;
61
+ }
62
+
63
+ for (const rawChunk of content) {
64
+ if (!isObject(rawChunk) || getString(rawChunk, "type") !== "tool_use") {
65
+ continue;
66
+ }
67
+
68
+ const toolName = getString(rawChunk, "name") ?? "tool";
69
+ const input = getObject(rawChunk, "input");
70
+ const description = input ? getString(input, "description") ?? "" : "";
71
+ const command = input ? getString(input, "command") ?? "" : "";
72
+
73
+ if (description && command) {
74
+ writeLine(`[tool] ${toolName}: ${description} (${command})`);
75
+ continue;
76
+ }
77
+ if (command) {
78
+ writeLine(`[tool] ${toolName}: ${command}`);
79
+ continue;
80
+ }
81
+ if (description) {
82
+ writeLine(`[tool] ${toolName}: ${description}`);
83
+ continue;
84
+ }
85
+ writeLine(`[tool] ${toolName}`);
86
+ }
87
+ };
88
+
89
+ const rl = createInterface({
90
+ input: process.stdin,
91
+ crlfDelay: Infinity
92
+ });
93
+
94
+ rl.on("line", (line: string) => {
95
+ if (!line.trim()) {
96
+ return;
97
+ }
98
+
99
+ if (process.env.WILE_STREAM_JSON === "true") {
100
+ writeLine(line);
101
+ return;
102
+ }
103
+
104
+ let parsed: unknown;
105
+ try {
106
+ parsed = JSON.parse(line);
107
+ } catch {
108
+ return;
109
+ }
110
+
111
+ if (!isObject(parsed)) {
112
+ return;
113
+ }
114
+
115
+ const payloadType = getString(parsed, "type");
116
+
117
+ if (payloadType === "system" && getString(parsed, "subtype") === "init") {
118
+ const model = getString(parsed, "model") ?? "unknown";
119
+ writeLine(`[system] model: ${model}`);
120
+ return;
121
+ }
122
+
123
+ if (payloadType === "assistant") {
124
+ const message = getObject(parsed, "message");
125
+ if (!message) {
126
+ return;
127
+ }
128
+ const content = message["content"];
129
+ extractToolUse(content);
130
+ extractText(content);
131
+ return;
132
+ }
133
+
134
+ if (payloadType === "user") {
135
+ const message = getObject(parsed, "message");
136
+ const content = message ? message["content"] : undefined;
137
+ if (!Array.isArray(content)) {
138
+ return;
139
+ }
140
+
141
+ for (const rawChunk of content) {
142
+ if (!isObject(rawChunk) || getString(rawChunk, "type") !== "tool_result") {
143
+ continue;
144
+ }
145
+
146
+ const toolUseId = getString(rawChunk, "tool_use_id") ?? "unknown";
147
+ const isError = rawChunk["is_error"] === true;
148
+ writeLine(`[tool-result] ${toolUseId} ${isError ? "error" : "ok"}`);
149
+ }
150
+ return;
151
+ }
152
+
153
+ if (payloadType === "result") {
154
+ const result = getString(parsed, "result");
155
+ if (result) {
156
+ writeLine(result);
157
+ }
158
+ }
159
+ });