wile 0.8.0 → 1.0.1
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/dist/agent/Dockerfile +1 -1
- package/dist/agent/README.md +20 -20
- package/dist/agent/entrypoint.sh +33 -21
- package/dist/agent/scripts/claude-stream.ts +159 -0
- package/dist/agent/scripts/codex-stream.ts +167 -0
- package/dist/agent/scripts/gemini-stream.ts +97 -0
- package/dist/agent/scripts/mock-claude.sh +48 -21
- package/dist/agent/scripts/mock-codex.sh +48 -21
- package/dist/agent/scripts/mock-gemini.sh +48 -21
- package/dist/agent/scripts/opencode-stream.ts +93 -0
- package/dist/agent/scripts/prompt-compact.md +36 -34
- package/dist/agent/scripts/prompt-preflight.md +0 -7
- package/dist/agent/scripts/prompt.md +26 -24
- package/dist/agent/scripts/test-additional-instructions.sh +4 -2
- package/dist/agent/scripts/test-env-project-docker.sh +5 -4
- package/dist/agent/scripts/test-iteration-limit.sh +4 -2
- package/dist/agent/scripts/test-prd-validation-docker.sh +151 -0
- package/dist/agent/scripts/test-preflight-claude-docker.sh +5 -4
- package/dist/agent/scripts/test-preflight-docker.sh +16 -13
- package/dist/agent/scripts/validate-compact.ts +134 -0
- package/dist/agent/scripts/validate-prd.ts +280 -0
- package/dist/agent/scripts/wile-compact.sh +6 -6
- package/dist/agent/scripts/wile-preflight.sh +4 -4
- package/dist/agent/scripts/wile.sh +4 -4
- package/dist/cli.js +236 -58
- package/package.json +10 -1
- package/dist/agent/scripts/claude-stream.js +0 -88
- package/dist/agent/scripts/codex-stream.js +0 -125
- package/dist/agent/scripts/gemini-stream.js +0 -63
- package/dist/agent/scripts/opencode-stream.js +0 -52
- package/dist/agent/scripts/validate-compact.js +0 -135
package/dist/agent/Dockerfile
CHANGED
|
@@ -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 \
|
package/dist/agent/README.md
CHANGED
|
@@ -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`
|
|
26
|
-
5. Loops through
|
|
27
|
-
- Picks
|
|
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
|
-
-
|
|
31
|
-
- Marks story as complete
|
|
30
|
+
- Marks story as done
|
|
32
31
|
- Logs learnings
|
|
33
|
-
|
|
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
|
|
37
|
+
Your repository must have a `.wile/prd.json` file:
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
your-repo/
|
|
41
41
|
├── .wile/
|
|
42
|
-
│ ├── prd.json #
|
|
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
|
-
"
|
|
52
|
+
"stories": [
|
|
53
53
|
{
|
|
54
|
-
"id":
|
|
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
|
-
"
|
|
62
|
-
"
|
|
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
|
|
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
|
-
- `
|
|
75
|
-
- `
|
|
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
|
|
|
@@ -134,9 +134,9 @@ For UI work, tell the agent how to verify:
|
|
|
134
134
|
|
|
135
135
|
## Output Files
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
Tracked artifacts:
|
|
138
138
|
|
|
139
|
-
-
|
|
139
|
+
- `.wile/prd.json` - Product requirements stories backlog
|
|
140
140
|
- `progress.txt` - Log of completed work and learnings
|
|
141
141
|
- `screenshots/` - Visual verification screenshots (gitignored)
|
|
142
142
|
|
|
@@ -157,5 +157,5 @@ git show origin/your-branch:.wile/progress.txt
|
|
|
157
157
|
Check story status:
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
git show origin/your-branch:.wile/prd.json | jq '.
|
|
160
|
+
git show origin/your-branch:.wile/prd.json | jq '.stories[] | {id, title, status}'
|
|
161
161
|
```
|
package/dist/agent/entrypoint.sh
CHANGED
|
@@ -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
|
|
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.
|
|
44
|
-
const
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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)) {
|
|
@@ -138,7 +147,6 @@ else
|
|
|
138
147
|
fi
|
|
139
148
|
|
|
140
149
|
MAX_ITERATIONS=${MAX_ITERATIONS:-25}
|
|
141
|
-
SCRIPT_DIR="/home/wile/scripts"
|
|
142
150
|
WORKSPACE="/home/wile/workspace"
|
|
143
151
|
|
|
144
152
|
if [ "${WILE_MOCK_CLAUDE:-}" = "true" ] && [ "$CODING_AGENT" = "CC" ]; then
|
|
@@ -357,24 +365,28 @@ echo "Checking for .wile/prd.json..."
|
|
|
357
365
|
if [ ! -f ".wile/prd.json" ]; then
|
|
358
366
|
echo "ERROR: .wile/prd.json not found!"
|
|
359
367
|
echo ""
|
|
360
|
-
echo "Your repository must have a .wile/prd.json file
|
|
361
|
-
echo "This file contains the
|
|
368
|
+
echo "Your repository must have a .wile/prd.json file."
|
|
369
|
+
echo "This file contains the stories for Wile to implement."
|
|
362
370
|
echo ""
|
|
363
371
|
echo "Example structure:"
|
|
364
372
|
echo ' {'
|
|
365
|
-
echo ' "
|
|
373
|
+
echo ' "stories": ['
|
|
366
374
|
echo ' {'
|
|
367
|
-
echo ' "id":
|
|
375
|
+
echo ' "id": 1,'
|
|
368
376
|
echo ' "title": "My feature",'
|
|
377
|
+
echo ' "description": "What this story delivers",'
|
|
369
378
|
echo ' "acceptanceCriteria": ["..."],'
|
|
370
|
-
echo ' "
|
|
371
|
-
echo ' "
|
|
379
|
+
echo ' "dependsOn": [],'
|
|
380
|
+
echo ' "status": "pending"'
|
|
372
381
|
echo ' }'
|
|
373
382
|
echo ' ]'
|
|
374
383
|
echo ' }'
|
|
375
384
|
exit 1
|
|
376
385
|
fi
|
|
377
386
|
|
|
387
|
+
echo "Validating .wile/prd.json..."
|
|
388
|
+
bun "$SCRIPT_DIR/validate-prd.ts" --path .wile/prd.json
|
|
389
|
+
|
|
378
390
|
# Set up .wile/screenshots directory
|
|
379
391
|
echo "Setting up .wile directory..."
|
|
380
392
|
mkdir -p .wile/screenshots
|
|
@@ -433,7 +445,7 @@ elif [ $EXIT_CODE -eq 1 ]; then
|
|
|
433
445
|
git commit -m "WIP: wile stopped at max iterations ($TIMESTAMP)
|
|
434
446
|
|
|
435
447
|
Partial work committed. Check .wile/progress.txt for details.
|
|
436
|
-
Some stories may still have
|
|
448
|
+
Some stories may still have status=\"pending\" in .wile/prd.json."
|
|
437
449
|
git push
|
|
438
450
|
fi
|
|
439
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
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
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 writeText = (text: string): void => {
|
|
24
|
+
process.stdout.write(text);
|
|
25
|
+
if (!text.endsWith("\n")) {
|
|
26
|
+
process.stdout.write("\n");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const formatChanges = (changes: unknown): string => {
|
|
31
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
for (const rawChange of changes) {
|
|
37
|
+
if (!isObject(rawChange)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const kind = getString(rawChange, "kind") ?? "update";
|
|
41
|
+
const path = getString(rawChange, "path") ?? "unknown";
|
|
42
|
+
parts.push(`${kind} ${path}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parts.join(", ");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const rl = createInterface({
|
|
49
|
+
input: process.stdin,
|
|
50
|
+
crlfDelay: Infinity
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
rl.on("line", (line: string) => {
|
|
54
|
+
if (!line.trim()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.env.WILE_STREAM_JSON === "true") {
|
|
59
|
+
writeLine(line);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let parsed: unknown;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(line);
|
|
66
|
+
} catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!isObject(parsed)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const eventType = getString(parsed, "type");
|
|
75
|
+
|
|
76
|
+
if (eventType === "thread.started") {
|
|
77
|
+
const threadId = getString(parsed, "thread_id");
|
|
78
|
+
if (threadId) {
|
|
79
|
+
writeLine(`[system] thread: ${threadId}`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (eventType === "turn.failed") {
|
|
85
|
+
const error = getObject(parsed, "error");
|
|
86
|
+
const message = error ? getString(error, "message") ?? "Unknown error" : "Unknown error";
|
|
87
|
+
writeLine(`[error] ${message}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (eventType === "error") {
|
|
92
|
+
const message = getString(parsed, "message") ?? "Unknown error";
|
|
93
|
+
writeLine(`[error] ${message}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const item = getObject(parsed, "item");
|
|
98
|
+
if (!item) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const itemType = getString(item, "type");
|
|
103
|
+
|
|
104
|
+
if (eventType === "item.completed" && itemType === "agent_message") {
|
|
105
|
+
const text = getString(item, "text");
|
|
106
|
+
if (text) {
|
|
107
|
+
writeText(text);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (eventType === "item.completed" && itemType === "reasoning") {
|
|
113
|
+
writeLine(`[thinking] ${getString(item, "text") ?? ""}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (itemType === "command_execution") {
|
|
118
|
+
const command = getString(item, "command") ?? "command";
|
|
119
|
+
if (eventType === "item.started") {
|
|
120
|
+
writeLine(`[tool] shell: ${command}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (eventType === "item.completed") {
|
|
125
|
+
const status = getString(item, "status") ?? "completed";
|
|
126
|
+
const exitCodeValue = item["exit_code"];
|
|
127
|
+
const exitCode =
|
|
128
|
+
typeof exitCodeValue === "number" || typeof exitCodeValue === "string"
|
|
129
|
+
? ` (exit ${String(exitCodeValue)})`
|
|
130
|
+
: "";
|
|
131
|
+
writeLine(`[tool-result] shell ${status}${exitCode}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (eventType === "item.completed" && itemType === "file_change") {
|
|
137
|
+
const detail = formatChanges(item["changes"]);
|
|
138
|
+
writeLine(`[tool] file_change${detail ? `: ${detail}` : ""}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (itemType === "mcp_tool_call") {
|
|
143
|
+
const server = getString(item, "server") ?? "mcp";
|
|
144
|
+
const tool = getString(item, "tool") ?? "tool";
|
|
145
|
+
|
|
146
|
+
if (eventType === "item.started") {
|
|
147
|
+
writeLine(`[tool] mcp ${server}/${tool}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (eventType === "item.completed") {
|
|
152
|
+
const status = getString(item, "status") ?? "completed";
|
|
153
|
+
writeLine(`[tool-result] mcp ${server}/${tool} ${status}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (itemType === "web_search" && eventType === "item.started") {
|
|
159
|
+
const query = getString(item, "query") ?? "";
|
|
160
|
+
writeLine(`[tool] web_search${query ? `: ${query}` : ""}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (eventType === "item.completed" && itemType === "error") {
|
|
165
|
+
writeLine(`[error] ${getString(item, "message") ?? "Unknown error"}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
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 writeContent = (content: string): void => {
|
|
24
|
+
process.stdout.write(content);
|
|
25
|
+
if (!content.endsWith("\n")) {
|
|
26
|
+
process.stdout.write("\n");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const rl = createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
crlfDelay: Infinity
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
rl.on("line", (line: string) => {
|
|
36
|
+
if (!line.trim()) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let parsed: unknown;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(line);
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isObject(parsed)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const payloadType = getString(parsed, "type");
|
|
52
|
+
|
|
53
|
+
if (payloadType === "init") {
|
|
54
|
+
const model = getString(parsed, "model") ?? "unknown";
|
|
55
|
+
writeLine(`[system] model: ${model}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (payloadType === "message") {
|
|
60
|
+
const role = getString(parsed, "role");
|
|
61
|
+
const content = getString(parsed, "content");
|
|
62
|
+
if (role === "assistant" && content) {
|
|
63
|
+
writeContent(content);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (payloadType === "tool_use") {
|
|
69
|
+
const tool = getString(parsed, "tool_name") ?? "tool";
|
|
70
|
+
const params = getObject(parsed, "parameters");
|
|
71
|
+
const detail = params && Object.keys(params).length > 0 ? `: ${JSON.stringify(params)}` : "";
|
|
72
|
+
writeLine(`[tool] ${tool}${detail}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (payloadType === "tool_result") {
|
|
77
|
+
const toolId = getString(parsed, "tool_id") ?? "unknown";
|
|
78
|
+
const status = getString(parsed, "status") ?? "unknown";
|
|
79
|
+
writeLine(`[tool-result] ${toolId} ${status}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (payloadType === "error") {
|
|
84
|
+
const message =
|
|
85
|
+
getString(parsed, "message") ??
|
|
86
|
+
(() => {
|
|
87
|
+
const error = getObject(parsed, "error");
|
|
88
|
+
if (!error) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return getString(error, "message") ?? getString(error, "error");
|
|
92
|
+
})() ??
|
|
93
|
+
"Unknown error";
|
|
94
|
+
|
|
95
|
+
writeLine(`[error] ${message}`);
|
|
96
|
+
}
|
|
97
|
+
});
|