@codexview/react 0.1.3 → 0.2.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 +5 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/changelog.md +22 -0
- package/package.json +2 -2
- package/LICENSE +0 -21
- package/docs/superpowers/plans/2026-05-15-claude-code-adapter-implementation.md +0 -2005
- package/docs/superpowers/plans/2026-05-15-codexview-implementation.md +0 -3903
- package/docs/superpowers/specs/2026-05-15-claude-code-adapter-design.md +0 -402
- package/docs/superpowers/specs/2026-05-15-codexview-design.md +0 -661
|
@@ -1,2005 +0,0 @@
|
|
|
1
|
-
# Claude Code Adapter Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Add a new playground adapter `adaptClaudeCode()` that converts `~/.claude/projects/<repo>/<sessionId>.jsonl` into the existing `ChatStreamEvent[]` union, so `<CodexTranscript />` can render Claude Code sessions with zero changes to `src/`.
|
|
6
|
-
|
|
7
|
-
**Architecture:** All adapter logic lives in `playground/adapter.mjs` (extend existing `detectFormat()` and `adapt()`). `playground/api.mjs` gains `~/.claude/projects/` scanning. `playground/src/App.tsx` adds a `claude-code` filter & badge. Raw Claude Code sample JSONL fixtures live in a new `fixtures/claude-code/` subfolder to keep them separate from the existing already-adapted fixtures consumed by `loadFixture.ts`.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node 20+, ESM modules, vitest (global APIs: `describe`/`it`/`expect`), React 18, TypeScript 5.5. No new dependencies.
|
|
10
|
-
|
|
11
|
-
**Spec:** [docs/superpowers/specs/2026-05-15-claude-code-adapter-design.md](../specs/2026-05-15-claude-code-adapter-design.md)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## File Structure
|
|
16
|
-
|
|
17
|
-
| File | Purpose | Action |
|
|
18
|
-
|---|---|---|
|
|
19
|
-
| `playground/adapter.mjs` | Add `adaptClaudeCode()`, extend `detectFormat()`, extend `adapt()` | Modify |
|
|
20
|
-
| `playground/adapter.claude-code.test.mjs` | Vitest unit tests for `adaptClaudeCode()` | Create |
|
|
21
|
-
| `playground/api.mjs` | Add `CLAUDE_ROOT` scanner + permission check | Modify |
|
|
22
|
-
| `playground/src/App.tsx` | Add `claude-code` filter option + badge | Modify |
|
|
23
|
-
| `fixtures/claude-code/short.jsonl` | Real-derived minimal Claude Code session | Create |
|
|
24
|
-
| `fixtures/claude-code/tool-heavy.jsonl` | Multi-turn with Bash/Edit/MultiEdit/TodoWrite/MCP | Create |
|
|
25
|
-
| `fixtures/claude-code/thinking-mixed.jsonl` | Plaintext + encrypted thinking blocks | Create |
|
|
26
|
-
| `fixtures/README.md` | Add note about Claude Code raw fixtures and anonymization rules | Modify |
|
|
27
|
-
|
|
28
|
-
Adapter test file lives in `playground/` (not `src/`) because the adapter is playground-only. Vitest's default discovery (`**/*.{test,spec}.?(c|m)[jt]s?(x)`) picks it up automatically.
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Task 1: Extend `detectFormat()` to recognize Claude Code lines
|
|
33
|
-
|
|
34
|
-
**Files:**
|
|
35
|
-
- Create: `playground/adapter.claude-code.test.mjs`
|
|
36
|
-
- Modify: `playground/adapter.mjs:41-49`
|
|
37
|
-
|
|
38
|
-
- [ ] **Step 1: Write the failing test**
|
|
39
|
-
|
|
40
|
-
Create `playground/adapter.claude-code.test.mjs`:
|
|
41
|
-
|
|
42
|
-
```js
|
|
43
|
-
import { describe, it, expect } from 'vitest';
|
|
44
|
-
import { adapt } from './adapter.mjs';
|
|
45
|
-
|
|
46
|
-
describe('detectFormat (via adapt)', () => {
|
|
47
|
-
it("returns format='claude-code' for Claude Code JSONL", () => {
|
|
48
|
-
const lines = [
|
|
49
|
-
{ type: 'user', uuid: 'u1', sessionId: 's1', parentUuid: null, timestamp: '2026-05-15T00:00:00Z',
|
|
50
|
-
message: { role: 'user', content: 'hi' } },
|
|
51
|
-
];
|
|
52
|
-
const { format } = adapt(lines);
|
|
53
|
-
expect(format).toBe('claude-code');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns format='claude-code' when queue-operation lines precede conversation lines", () => {
|
|
57
|
-
const lines = [
|
|
58
|
-
{ type: 'queue-operation', operation: 'enqueue', timestamp: '2026-05-15T00:00:00Z',
|
|
59
|
-
sessionId: 's1', content: 'queued' },
|
|
60
|
-
{ type: 'user', uuid: 'u1', sessionId: 's1', parentUuid: null,
|
|
61
|
-
timestamp: '2026-05-15T00:00:01Z', message: { role: 'user', content: 'hi' } },
|
|
62
|
-
];
|
|
63
|
-
expect(adapt(lines).format).toBe('claude-code');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("still returns format='rollout' for Codex CLI JSONL", () => {
|
|
67
|
-
const lines = [
|
|
68
|
-
{ type: 'session_meta', timestamp: '2026-05-15T00:00:00Z', payload: { id: 'thread-x' } },
|
|
69
|
-
];
|
|
70
|
-
const { format } = adapt(lines);
|
|
71
|
-
expect(format).toBe('rollout');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("returns format='unknown' for empty input", () => {
|
|
75
|
-
expect(adapt([]).format).toBe('unknown');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
- [ ] **Step 2: Run the test — expect it to fail**
|
|
81
|
-
|
|
82
|
-
Run:
|
|
83
|
-
```bash
|
|
84
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
85
|
-
```
|
|
86
|
-
Expected: First test fails OR fourth test fails (depending on order) — detection logic is missing or incorrect. Second & third pass.
|
|
87
|
-
|
|
88
|
-
- [ ] **Step 3: Update `detectFormat()` and `adapt()` in `playground/adapter.mjs`**
|
|
89
|
-
|
|
90
|
-
Replace the `detectFormat` function:
|
|
91
|
-
|
|
92
|
-
```js
|
|
93
|
-
const detectFormat = (lines) => {
|
|
94
|
-
for (const line of lines) {
|
|
95
|
-
if (line && typeof line === 'object') {
|
|
96
|
-
// Claude Code: every line carries `sessionId` (queue-operation, system,
|
|
97
|
-
// last-prompt, user, assistant, attachment, …). Codex rollout and
|
|
98
|
-
// codex-team logs do not.
|
|
99
|
-
if ('sessionId' in line) return 'claude-code';
|
|
100
|
-
// Codex rollout
|
|
101
|
-
if ('type' in line && ('payload' in line || 'timestamp' in line)) return 'rollout';
|
|
102
|
-
// AgentWeb codex-team status log
|
|
103
|
-
if ('event' in line && 'at' in line) return 'codex-team';
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return 'unknown';
|
|
107
|
-
};
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Update `adapt()` (the bottom of the file) to dispatch the new format:
|
|
111
|
-
|
|
112
|
-
```js
|
|
113
|
-
export function adapt(rawLines) {
|
|
114
|
-
const fmt = detectFormat(rawLines);
|
|
115
|
-
if (fmt === 'rollout') return { format: 'rollout', events: adaptRollout(rawLines) };
|
|
116
|
-
if (fmt === 'codex-team') return { format: 'codex-team', events: adaptCodexTeam(rawLines) };
|
|
117
|
-
if (fmt === 'claude-code') return { format: 'claude-code', events: adaptClaudeCode(rawLines) };
|
|
118
|
-
return { format: 'unknown', events: rawLines.map((p, i) => ({ type: 'raw', payload: p, at: Date.now() + i })) };
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Add a stub at the bottom of the file (full implementation comes in later tasks):
|
|
123
|
-
|
|
124
|
-
```js
|
|
125
|
-
function adaptClaudeCode(lines) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
- [ ] **Step 4: Run the test — expect it to pass**
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
134
|
-
```
|
|
135
|
-
Expected: All three tests pass.
|
|
136
|
-
|
|
137
|
-
- [ ] **Step 5: Commit**
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
141
|
-
git commit -m "feat(adapter): detect Claude Code JSONL format"
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## Task 2: Emit `thread_started` from `sessionId`
|
|
147
|
-
|
|
148
|
-
**Files:**
|
|
149
|
-
- Modify: `playground/adapter.mjs` (the `adaptClaudeCode` stub from Task 1)
|
|
150
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
151
|
-
|
|
152
|
-
- [ ] **Step 1: Write the failing test**
|
|
153
|
-
|
|
154
|
-
Append to `playground/adapter.claude-code.test.mjs`:
|
|
155
|
-
|
|
156
|
-
```js
|
|
157
|
-
describe('adaptClaudeCode · thread', () => {
|
|
158
|
-
it('emits thread_started with sessionId from first line', () => {
|
|
159
|
-
const lines = [
|
|
160
|
-
{ type: 'attachment', uuid: 'a1', sessionId: 'sess-abc', parentUuid: null,
|
|
161
|
-
timestamp: '2026-05-15T00:00:00Z', attachment: { type: 'hook_success' } },
|
|
162
|
-
];
|
|
163
|
-
const { events } = adapt(lines);
|
|
164
|
-
expect(events[0]).toMatchObject({ type: 'thread_started', threadId: 'sess-abc' });
|
|
165
|
-
expect(typeof events[0].at).toBe('number');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('emits thread_started only once even across many lines', () => {
|
|
169
|
-
const lines = [
|
|
170
|
-
{ type: 'attachment', uuid: 'a1', sessionId: 'sess-X', parentUuid: null, timestamp: '2026-05-15T00:00:00Z', attachment: { type: 'hook_success' } },
|
|
171
|
-
{ type: 'attachment', uuid: 'a2', sessionId: 'sess-X', parentUuid: 'a1', timestamp: '2026-05-15T00:00:01Z', attachment: { type: 'hook_success' } },
|
|
172
|
-
];
|
|
173
|
-
const { events } = adapt(lines);
|
|
174
|
-
const threadStarts = events.filter((e) => e.type === 'thread_started');
|
|
175
|
-
expect(threadStarts).toHaveLength(1);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
- [ ] **Step 2: Run — expect failures**
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
184
|
-
```
|
|
185
|
-
Expected: Both new tests fail (`events[0]` is undefined).
|
|
186
|
-
|
|
187
|
-
- [ ] **Step 3: Implement scaffold + thread_started**
|
|
188
|
-
|
|
189
|
-
Replace the `adaptClaudeCode` stub with:
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
function adaptClaudeCode(lines) {
|
|
193
|
-
const out = [];
|
|
194
|
-
let threadStarted = false;
|
|
195
|
-
|
|
196
|
-
for (const line of lines) {
|
|
197
|
-
if (!line || typeof line !== 'object') continue;
|
|
198
|
-
if (line.isSidechain === true) continue;
|
|
199
|
-
|
|
200
|
-
const at = epoch(line.timestamp);
|
|
201
|
-
|
|
202
|
-
if (!threadStarted && typeof line.sessionId === 'string' && line.sessionId.length > 0) {
|
|
203
|
-
out.push({ type: 'thread_started', threadId: line.sessionId, at });
|
|
204
|
-
threadStarted = true;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return out;
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
- [ ] **Step 4: Run — expect pass**
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
216
|
-
```
|
|
217
|
-
Expected: All tests pass.
|
|
218
|
-
|
|
219
|
-
- [ ] **Step 5: Commit**
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
223
|
-
git commit -m "feat(adapter): emit thread_started for Claude Code"
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
---
|
|
227
|
-
|
|
228
|
-
## Task 3: Drop non-transcript types and sidechain lines
|
|
229
|
-
|
|
230
|
-
**Files:**
|
|
231
|
-
- Modify: `playground/adapter.mjs`
|
|
232
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
233
|
-
|
|
234
|
-
- [ ] **Step 1: Write the failing test**
|
|
235
|
-
|
|
236
|
-
Append to the test file:
|
|
237
|
-
|
|
238
|
-
```js
|
|
239
|
-
describe('adaptClaudeCode · filtering', () => {
|
|
240
|
-
it('drops attachment / system / last-prompt / queue-operation (only thread_started emits)', () => {
|
|
241
|
-
const base = { uuid: 'u1', sessionId: 'sess', parentUuid: null, timestamp: '2026-05-15T00:00:00Z' };
|
|
242
|
-
const lines = [
|
|
243
|
-
{ ...base, type: 'attachment', attachment: { type: 'hook_success' } },
|
|
244
|
-
{ ...base, type: 'system', uuid: 'u2', content: 'whatever' },
|
|
245
|
-
{ ...base, type: 'last-prompt', uuid: 'u3', lastPrompt: 'draft' },
|
|
246
|
-
{ ...base, type: 'queue-operation', uuid: 'u4', operation: 'enqueue', content: 'queued' },
|
|
247
|
-
];
|
|
248
|
-
const { events } = adapt(lines);
|
|
249
|
-
expect(events.map((e) => e.type)).toEqual(['thread_started']);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('drops lines where isSidechain === true entirely (even the first one)', () => {
|
|
253
|
-
const lines = [
|
|
254
|
-
{ type: 'user', uuid: 'u1', sessionId: 'sess', parentUuid: null, isSidechain: true,
|
|
255
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'side' } },
|
|
256
|
-
{ type: 'attachment', uuid: 'a1', sessionId: 'sess', parentUuid: null, isSidechain: false,
|
|
257
|
-
timestamp: '2026-05-15T00:00:01Z', attachment: { type: 'hook_success' } },
|
|
258
|
-
];
|
|
259
|
-
const { events } = adapt(lines);
|
|
260
|
-
expect(events).toHaveLength(1);
|
|
261
|
-
expect(events[0]).toMatchObject({ type: 'thread_started', threadId: 'sess' });
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
- [ ] **Step 2: Run — expect failures only on the sidechain test**
|
|
267
|
-
|
|
268
|
-
The first test should already pass (we currently only emit thread_started and skip everything else). The second test should pass too if `isSidechain === true` is already skipped (it is, from Task 2). Run and confirm:
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
Expected: Both pass. The purpose of this task is to **lock these invariants into tests** before later tasks (which will add emissions) could accidentally regress them.
|
|
275
|
-
|
|
276
|
-
- [ ] **Step 3: Add explicit type-filter in code (defensive)**
|
|
277
|
-
|
|
278
|
-
In `adaptClaudeCode`, after the `isSidechain` check, add:
|
|
279
|
-
|
|
280
|
-
```js
|
|
281
|
-
const skipTypes = new Set(['attachment', 'system', 'last-prompt', 'queue-operation']);
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
Move it outside the loop (top of function), then inside the loop after the `at` line:
|
|
285
|
-
|
|
286
|
-
```js
|
|
287
|
-
if (skipTypes.has(line.type)) continue;
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
This is currently a no-op (nothing else emits yet) but is the defensive guard for later tasks.
|
|
291
|
-
|
|
292
|
-
- [ ] **Step 4: Re-run tests**
|
|
293
|
-
|
|
294
|
-
```bash
|
|
295
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
296
|
-
```
|
|
297
|
-
Expected: All pass.
|
|
298
|
-
|
|
299
|
-
- [ ] **Step 5: Commit**
|
|
300
|
-
|
|
301
|
-
```bash
|
|
302
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
303
|
-
git commit -m "feat(adapter): filter non-transcript Claude Code event types"
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Task 4: Text-user message → `turn_started` + `user_message`
|
|
309
|
-
|
|
310
|
-
**Files:**
|
|
311
|
-
- Modify: `playground/adapter.mjs`
|
|
312
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
313
|
-
|
|
314
|
-
- [ ] **Step 1: Write the failing test**
|
|
315
|
-
|
|
316
|
-
Append:
|
|
317
|
-
|
|
318
|
-
```js
|
|
319
|
-
describe('adaptClaudeCode · text-user (turn boundary)', () => {
|
|
320
|
-
it('emits turn_started + user_message for a string-content user message', () => {
|
|
321
|
-
const lines = [
|
|
322
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
323
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'hello' } },
|
|
324
|
-
];
|
|
325
|
-
const { events } = adapt(lines);
|
|
326
|
-
expect(events.map((e) => e.type)).toEqual(['thread_started', 'turn_started', 'user_message']);
|
|
327
|
-
expect(events[1]).toMatchObject({ turnId: 'u-1' });
|
|
328
|
-
expect(events[2]).toMatchObject({ turnId: 'u-1', itemId: 'u-1', text: 'hello' });
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('treats user with content[]={type:"text",text:...} as a text-user too', () => {
|
|
332
|
-
const lines = [
|
|
333
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
334
|
-
timestamp: '2026-05-15T00:00:00Z',
|
|
335
|
-
message: { role: 'user', content: [{ type: 'text', text: 'multiline' }] } },
|
|
336
|
-
];
|
|
337
|
-
const { events } = adapt(lines);
|
|
338
|
-
const userMsgs = events.filter((e) => e.type === 'user_message');
|
|
339
|
-
expect(userMsgs).toHaveLength(1);
|
|
340
|
-
expect(userMsgs[0]).toMatchObject({ text: 'multiline', itemId: 'u-1:0' });
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('does NOT open a new turn for user content that is a tool_result', () => {
|
|
344
|
-
const lines = [
|
|
345
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
346
|
-
timestamp: '2026-05-15T00:00:00Z',
|
|
347
|
-
message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: 'ok' }] } },
|
|
348
|
-
];
|
|
349
|
-
const { events } = adapt(lines);
|
|
350
|
-
expect(events.find((e) => e.type === 'turn_started')).toBeUndefined();
|
|
351
|
-
expect(events.find((e) => e.type === 'user_message')).toBeUndefined();
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
- [ ] **Step 2: Run — expect failures**
|
|
357
|
-
|
|
358
|
-
```bash
|
|
359
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Expected: 3 new failures.
|
|
363
|
-
|
|
364
|
-
- [ ] **Step 3: Implement text-user handling**
|
|
365
|
-
|
|
366
|
-
In `adaptClaudeCode`, before the loop add state:
|
|
367
|
-
|
|
368
|
-
```js
|
|
369
|
-
let threadStarted = false;
|
|
370
|
-
let currentTurnId = null;
|
|
371
|
-
let turnUsage = null; // { lastInput, sumOutput }
|
|
372
|
-
const pending = new Map(); // tool_use.id -> { kind, ... }
|
|
373
|
-
|
|
374
|
-
const closeTurn = (at) => {
|
|
375
|
-
if (!currentTurnId) return;
|
|
376
|
-
const evt = { type: 'turn_completed', turnId: currentTurnId, at };
|
|
377
|
-
if (turnUsage) {
|
|
378
|
-
evt.usage = {
|
|
379
|
-
inputTokens: turnUsage.lastInput || 0,
|
|
380
|
-
outputTokens: turnUsage.sumOutput || 0,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
out.push(evt);
|
|
384
|
-
currentTurnId = null;
|
|
385
|
-
turnUsage = null;
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
const isTextUser = (msg) => {
|
|
389
|
-
const content = msg?.content;
|
|
390
|
-
if (typeof content === 'string') return true;
|
|
391
|
-
if (!Array.isArray(content) || content.length === 0) return false;
|
|
392
|
-
return content.every((c) => c && c.type === 'text');
|
|
393
|
-
};
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
Inside the loop, after the existing `thread_started` block, add:
|
|
397
|
-
|
|
398
|
-
```js
|
|
399
|
-
if (line.type === 'user' && line.message) {
|
|
400
|
-
if (isTextUser(line.message)) {
|
|
401
|
-
// closeTurn intentionally NOT called here yet — Task 5 adds multi-turn closure.
|
|
402
|
-
currentTurnId = String(line.uuid || `cc-turn-${out.length}`);
|
|
403
|
-
turnUsage = { lastInput: 0, sumOutput: 0 };
|
|
404
|
-
out.push({ type: 'turn_started', turnId: currentTurnId, at });
|
|
405
|
-
|
|
406
|
-
const content = line.message.content;
|
|
407
|
-
if (typeof content === 'string') {
|
|
408
|
-
out.push({
|
|
409
|
-
type: 'user_message', turnId: currentTurnId, itemId: currentTurnId, text: content, at,
|
|
410
|
-
});
|
|
411
|
-
} else {
|
|
412
|
-
content.forEach((c, idx) => {
|
|
413
|
-
out.push({
|
|
414
|
-
type: 'user_message',
|
|
415
|
-
turnId: currentTurnId,
|
|
416
|
-
itemId: `${currentTurnId}:${idx}`,
|
|
417
|
-
text: String(c.text || ''),
|
|
418
|
-
at,
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
// Non-text-user (tool_result) handled in a later task.
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
- [ ] **Step 4: Run — expect pass**
|
|
430
|
-
|
|
431
|
-
```bash
|
|
432
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
433
|
-
```
|
|
434
|
-
Expected: All pass.
|
|
435
|
-
|
|
436
|
-
- [ ] **Step 5: Commit**
|
|
437
|
-
|
|
438
|
-
```bash
|
|
439
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
440
|
-
git commit -m "feat(adapter): map text-user to turn_started + user_message"
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## Task 5: Two text-user messages → close previous turn first
|
|
446
|
-
|
|
447
|
-
**Files:**
|
|
448
|
-
- Modify: `playground/adapter.mjs`
|
|
449
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
450
|
-
|
|
451
|
-
- [ ] **Step 1: Write the failing test**
|
|
452
|
-
|
|
453
|
-
Append:
|
|
454
|
-
|
|
455
|
-
```js
|
|
456
|
-
describe('adaptClaudeCode · multi-turn', () => {
|
|
457
|
-
it('closes the previous turn before opening a new one', () => {
|
|
458
|
-
const lines = [
|
|
459
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
460
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'first' } },
|
|
461
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'u-1',
|
|
462
|
-
timestamp: '2026-05-15T00:01:00Z', message: { role: 'user', content: 'second' } },
|
|
463
|
-
];
|
|
464
|
-
const { events } = adapt(lines);
|
|
465
|
-
const types = events.map((e) => e.type);
|
|
466
|
-
expect(types).toEqual([
|
|
467
|
-
'thread_started',
|
|
468
|
-
'turn_started', 'user_message', // first turn opens
|
|
469
|
-
'turn_completed', // first turn closes
|
|
470
|
-
'turn_started', 'user_message', // second turn opens
|
|
471
|
-
]);
|
|
472
|
-
expect(events[3]).toMatchObject({ type: 'turn_completed', turnId: 'u-1' });
|
|
473
|
-
expect(events[4]).toMatchObject({ type: 'turn_started', turnId: 'u-2' });
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
- [ ] **Step 2: Run — expect failure**
|
|
479
|
-
|
|
480
|
-
```bash
|
|
481
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
482
|
-
```
|
|
483
|
-
Expected: Fails (no `turn_completed` between the two turns).
|
|
484
|
-
|
|
485
|
-
- [ ] **Step 3: Add `closeTurn(at)` at the top of the text-user branch**
|
|
486
|
-
|
|
487
|
-
In `adaptClaudeCode`, inside the `if (isTextUser(line.message))` block, **before** the `currentTurnId = …` assignment, insert:
|
|
488
|
-
|
|
489
|
-
```js
|
|
490
|
-
closeTurn(at);
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
- [ ] **Step 4: Run — expect pass**
|
|
494
|
-
|
|
495
|
-
```bash
|
|
496
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
497
|
-
```
|
|
498
|
-
Expected: All pass.
|
|
499
|
-
|
|
500
|
-
- [ ] **Step 5: Commit**
|
|
501
|
-
|
|
502
|
-
```bash
|
|
503
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
504
|
-
git commit -m "feat(adapter): close previous Claude Code turn on new text-user"
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
---
|
|
508
|
-
|
|
509
|
-
## Task 6: Close dangling turn at end of file
|
|
510
|
-
|
|
511
|
-
**Files:**
|
|
512
|
-
- Modify: `playground/adapter.mjs`
|
|
513
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
514
|
-
|
|
515
|
-
- [ ] **Step 1: Write the failing test**
|
|
516
|
-
|
|
517
|
-
Append:
|
|
518
|
-
|
|
519
|
-
```js
|
|
520
|
-
describe('adaptClaudeCode · EOF closure', () => {
|
|
521
|
-
it('closes a dangling turn at end-of-file', () => {
|
|
522
|
-
const lines = [
|
|
523
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
524
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'only' } },
|
|
525
|
-
];
|
|
526
|
-
const { events } = adapt(lines);
|
|
527
|
-
expect(events[events.length - 1]).toMatchObject({ type: 'turn_completed', turnId: 'u-1' });
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it('does not emit a stray turn_completed when there were no turns', () => {
|
|
531
|
-
const lines = [
|
|
532
|
-
{ type: 'attachment', uuid: 'a1', sessionId: 'sess', parentUuid: null,
|
|
533
|
-
timestamp: '2026-05-15T00:00:00Z', attachment: { type: 'hook_success' } },
|
|
534
|
-
];
|
|
535
|
-
const { events } = adapt(lines);
|
|
536
|
-
expect(events.some((e) => e.type === 'turn_completed')).toBe(false);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
- [ ] **Step 2: Run — expect failure**
|
|
542
|
-
|
|
543
|
-
```bash
|
|
544
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
545
|
-
```
|
|
546
|
-
Expected: First new test fails (no trailing turn_completed). Second passes.
|
|
547
|
-
|
|
548
|
-
- [ ] **Step 3: Add EOF closure**
|
|
549
|
-
|
|
550
|
-
At the very bottom of `adaptClaudeCode`, **before** `return out`:
|
|
551
|
-
|
|
552
|
-
```js
|
|
553
|
-
if (currentTurnId) {
|
|
554
|
-
const lastAt = out.length > 0 ? out[out.length - 1].at : Date.now();
|
|
555
|
-
closeTurn(lastAt);
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
- [ ] **Step 4: Run — expect pass**
|
|
560
|
-
|
|
561
|
-
```bash
|
|
562
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
563
|
-
```
|
|
564
|
-
Expected: All pass.
|
|
565
|
-
|
|
566
|
-
- [ ] **Step 5: Commit**
|
|
567
|
-
|
|
568
|
-
```bash
|
|
569
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
570
|
-
git commit -m "feat(adapter): close dangling Claude Code turn at EOF"
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
---
|
|
574
|
-
|
|
575
|
-
## Task 7: Assistant text content → `agent_message`
|
|
576
|
-
|
|
577
|
-
**Files:**
|
|
578
|
-
- Modify: `playground/adapter.mjs`
|
|
579
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
580
|
-
|
|
581
|
-
- [ ] **Step 1: Write the failing test**
|
|
582
|
-
|
|
583
|
-
Append:
|
|
584
|
-
|
|
585
|
-
```js
|
|
586
|
-
describe('adaptClaudeCode · assistant text', () => {
|
|
587
|
-
it('emits agent_message for each text block in assistant content[]', () => {
|
|
588
|
-
const lines = [
|
|
589
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
590
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
591
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
592
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
593
|
-
message: { role: 'assistant', model: 'claude-opus-4-7',
|
|
594
|
-
content: [{ type: 'text', text: 'answer1' }, { type: 'text', text: 'answer2' }],
|
|
595
|
-
usage: { input_tokens: 0, output_tokens: 0 } } },
|
|
596
|
-
];
|
|
597
|
-
const { events } = adapt(lines);
|
|
598
|
-
const am = events.filter((e) => e.type === 'agent_message');
|
|
599
|
-
expect(am).toHaveLength(2);
|
|
600
|
-
expect(am[0]).toMatchObject({ turnId: 'u-1', itemId: 'a-1:0', text: 'answer1', partial: false });
|
|
601
|
-
expect(am[1]).toMatchObject({ turnId: 'u-1', itemId: 'a-1:1', text: 'answer2', partial: false });
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it('ignores assistant messages that arrive without an open turn', () => {
|
|
605
|
-
const lines = [
|
|
606
|
-
{ type: 'assistant', uuid: 'a-orphan', sessionId: 'sess', parentUuid: null,
|
|
607
|
-
timestamp: '2026-05-15T00:00:00Z',
|
|
608
|
-
message: { role: 'assistant', content: [{ type: 'text', text: 'orphan' }] } },
|
|
609
|
-
];
|
|
610
|
-
const { events } = adapt(lines);
|
|
611
|
-
expect(events.some((e) => e.type === 'agent_message')).toBe(false);
|
|
612
|
-
});
|
|
613
|
-
});
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
- [ ] **Step 2: Run — expect failures**
|
|
617
|
-
|
|
618
|
-
```bash
|
|
619
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
620
|
-
```
|
|
621
|
-
Expected: First fails (no agent_message). Second passes.
|
|
622
|
-
|
|
623
|
-
- [ ] **Step 3: Implement assistant text branch**
|
|
624
|
-
|
|
625
|
-
In `adaptClaudeCode`, after the `if (line.type === 'user' && line.message) { … continue }` block, add:
|
|
626
|
-
|
|
627
|
-
```js
|
|
628
|
-
if (line.type === 'assistant' && line.message) {
|
|
629
|
-
if (!currentTurnId) continue;
|
|
630
|
-
const content = line.message.content;
|
|
631
|
-
if (!Array.isArray(content)) continue;
|
|
632
|
-
const asstUuid = String(line.uuid || `cc-a-${out.length}`);
|
|
633
|
-
|
|
634
|
-
content.forEach((c, idx) => {
|
|
635
|
-
if (!c || typeof c !== 'object') return;
|
|
636
|
-
const itemId = `${asstUuid}:${idx}`;
|
|
637
|
-
if (c.type === 'text') {
|
|
638
|
-
out.push({
|
|
639
|
-
type: 'agent_message',
|
|
640
|
-
turnId: currentTurnId,
|
|
641
|
-
itemId,
|
|
642
|
-
text: String(c.text || ''),
|
|
643
|
-
partial: false,
|
|
644
|
-
at,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
// thinking & tool_use handled in later tasks
|
|
648
|
-
});
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
- [ ] **Step 4: Run — expect pass**
|
|
654
|
-
|
|
655
|
-
```bash
|
|
656
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
- [ ] **Step 5: Commit**
|
|
660
|
-
|
|
661
|
-
```bash
|
|
662
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
663
|
-
git commit -m "feat(adapter): map assistant text content to agent_message"
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
## Task 8: Assistant thinking — plaintext → `reasoning`, empty → drop
|
|
669
|
-
|
|
670
|
-
**Files:**
|
|
671
|
-
- Modify: `playground/adapter.mjs` (the assistant `content.forEach` from Task 7)
|
|
672
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
673
|
-
|
|
674
|
-
- [ ] **Step 1: Write the failing test**
|
|
675
|
-
|
|
676
|
-
Append:
|
|
677
|
-
|
|
678
|
-
```js
|
|
679
|
-
describe('adaptClaudeCode · thinking', () => {
|
|
680
|
-
it('emits reasoning for non-empty thinking', () => {
|
|
681
|
-
const lines = [
|
|
682
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
683
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
684
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
685
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
686
|
-
message: { role: 'assistant',
|
|
687
|
-
content: [{ type: 'thinking', thinking: 'pondering...', signature: 'sig' }] } },
|
|
688
|
-
];
|
|
689
|
-
const { events } = adapt(lines);
|
|
690
|
-
const reasoning = events.filter((e) => e.type === 'reasoning');
|
|
691
|
-
expect(reasoning).toHaveLength(1);
|
|
692
|
-
expect(reasoning[0]).toMatchObject({ text: 'pondering...', itemId: 'a-1:0', partial: false });
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it('drops thinking blocks whose .thinking is an empty string (encrypted)', () => {
|
|
696
|
-
const lines = [
|
|
697
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
698
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
699
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
700
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
701
|
-
message: { role: 'assistant',
|
|
702
|
-
content: [{ type: 'thinking', thinking: '', signature: 'sig' }] } },
|
|
703
|
-
];
|
|
704
|
-
const { events } = adapt(lines);
|
|
705
|
-
expect(events.some((e) => e.type === 'reasoning')).toBe(false);
|
|
706
|
-
});
|
|
707
|
-
});
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
- [ ] **Step 2: Run — expect failures**
|
|
711
|
-
|
|
712
|
-
Expected: First fails (no reasoning emitted), second passes vacuously.
|
|
713
|
-
|
|
714
|
-
- [ ] **Step 3: Extend the content branch**
|
|
715
|
-
|
|
716
|
-
Inside the `content.forEach((c, idx) => {…})` block from Task 7, after the `if (c.type === 'text')` branch, add:
|
|
717
|
-
|
|
718
|
-
```js
|
|
719
|
-
if (c.type === 'thinking') {
|
|
720
|
-
const text = typeof c.thinking === 'string' ? c.thinking : '';
|
|
721
|
-
if (!text) return; // encrypted/empty — drop
|
|
722
|
-
out.push({
|
|
723
|
-
type: 'reasoning',
|
|
724
|
-
turnId: currentTurnId,
|
|
725
|
-
itemId,
|
|
726
|
-
text,
|
|
727
|
-
partial: false,
|
|
728
|
-
at,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
- [ ] **Step 4: Run — expect pass**
|
|
734
|
-
|
|
735
|
-
- [ ] **Step 5: Commit**
|
|
736
|
-
|
|
737
|
-
```bash
|
|
738
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
739
|
-
git commit -m "feat(adapter): emit reasoning for plaintext thinking only"
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
---
|
|
743
|
-
|
|
744
|
-
## Task 9: Aggregate token usage per turn
|
|
745
|
-
|
|
746
|
-
**Files:**
|
|
747
|
-
- Modify: `playground/adapter.mjs` (assistant branch)
|
|
748
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
749
|
-
|
|
750
|
-
- [ ] **Step 1: Write the failing test**
|
|
751
|
-
|
|
752
|
-
Append:
|
|
753
|
-
|
|
754
|
-
```js
|
|
755
|
-
describe('adaptClaudeCode · usage', () => {
|
|
756
|
-
it('attaches summed output_tokens and last input_tokens to turn_completed', () => {
|
|
757
|
-
const mkA = (uuid, input, output) => ({
|
|
758
|
-
type: 'assistant', uuid, sessionId: 'sess', parentUuid: null,
|
|
759
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
760
|
-
message: { role: 'assistant', content: [{ type: 'text', text: 'x' }],
|
|
761
|
-
usage: { input_tokens: input, output_tokens: output } },
|
|
762
|
-
});
|
|
763
|
-
const lines = [
|
|
764
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
765
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'go' } },
|
|
766
|
-
mkA('a-1', 100, 50),
|
|
767
|
-
mkA('a-2', 200, 30),
|
|
768
|
-
mkA('a-3', 250, 20),
|
|
769
|
-
];
|
|
770
|
-
const { events } = adapt(lines);
|
|
771
|
-
const done = events.find((e) => e.type === 'turn_completed');
|
|
772
|
-
expect(done.usage).toEqual({ inputTokens: 250, outputTokens: 100 });
|
|
773
|
-
});
|
|
774
|
-
});
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
- [ ] **Step 2: Run — expect failure**
|
|
778
|
-
|
|
779
|
-
- [ ] **Step 3: Track usage inside the assistant branch**
|
|
780
|
-
|
|
781
|
-
Right after `if (!currentTurnId) continue;` inside the assistant branch, before the `content` parsing, insert:
|
|
782
|
-
|
|
783
|
-
```js
|
|
784
|
-
const usage = line.message.usage;
|
|
785
|
-
if (usage && turnUsage) {
|
|
786
|
-
if (typeof usage.input_tokens === 'number') turnUsage.lastInput = usage.input_tokens;
|
|
787
|
-
if (typeof usage.output_tokens === 'number') turnUsage.sumOutput = (turnUsage.sumOutput || 0) + usage.output_tokens;
|
|
788
|
-
}
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
- [ ] **Step 4: Run — expect pass**
|
|
792
|
-
|
|
793
|
-
- [ ] **Step 5: Commit**
|
|
794
|
-
|
|
795
|
-
```bash
|
|
796
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
797
|
-
git commit -m "feat(adapter): aggregate Claude Code token usage per turn"
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
---
|
|
801
|
-
|
|
802
|
-
## Task 10: `Bash` tool → `exec_command_begin` + `exec_command_end`
|
|
803
|
-
|
|
804
|
-
**Files:**
|
|
805
|
-
- Modify: `playground/adapter.mjs` (assistant content & user content branches)
|
|
806
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
807
|
-
|
|
808
|
-
- [ ] **Step 1: Write the failing test**
|
|
809
|
-
|
|
810
|
-
Append:
|
|
811
|
-
|
|
812
|
-
```js
|
|
813
|
-
describe('adaptClaudeCode · Bash tool', () => {
|
|
814
|
-
it('maps tool_use(Bash) + tool_result(ok) to exec_command_begin + exec_command_end', () => {
|
|
815
|
-
const lines = [
|
|
816
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
817
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
818
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
819
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
820
|
-
message: { role: 'assistant', content: [
|
|
821
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Bash',
|
|
822
|
-
input: { command: 'ls -la', description: 'list' } },
|
|
823
|
-
] } },
|
|
824
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
825
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
826
|
-
message: { role: 'user', content: [
|
|
827
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'total 0\n', is_error: false },
|
|
828
|
-
] } },
|
|
829
|
-
];
|
|
830
|
-
const { events } = adapt(lines);
|
|
831
|
-
const begin = events.find((e) => e.type === 'exec_command_begin');
|
|
832
|
-
const end = events.find((e) => e.type === 'exec_command_end');
|
|
833
|
-
expect(begin).toMatchObject({ callId: 'tu-1', command: 'ls -la' });
|
|
834
|
-
expect(end).toMatchObject({ callId: 'tu-1', exit: 0, stdout: 'total 0\n', stderr: '' });
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it('maps tool_result(is_error=true) to exit=1 and routes content to stderr', () => {
|
|
838
|
-
const lines = [
|
|
839
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
840
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
841
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
842
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
843
|
-
message: { role: 'assistant', content: [
|
|
844
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Bash', input: { command: 'false' } },
|
|
845
|
-
] } },
|
|
846
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
847
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
848
|
-
message: { role: 'user', content: [
|
|
849
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'oh no', is_error: true },
|
|
850
|
-
] } },
|
|
851
|
-
];
|
|
852
|
-
const { events } = adapt(lines);
|
|
853
|
-
const end = events.find((e) => e.type === 'exec_command_end');
|
|
854
|
-
expect(end).toMatchObject({ exit: 1, stdout: '', stderr: 'oh no' });
|
|
855
|
-
});
|
|
856
|
-
});
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
- [ ] **Step 2: Run — expect failures**
|
|
860
|
-
|
|
861
|
-
- [ ] **Step 3: Add a `toolResultText()` helper and a tool-use dispatcher**
|
|
862
|
-
|
|
863
|
-
Near the top of `adaptClaudeCode` (before the `for` loop), add helpers:
|
|
864
|
-
|
|
865
|
-
```js
|
|
866
|
-
const toolResultText = (item) => {
|
|
867
|
-
const c = item?.content;
|
|
868
|
-
if (typeof c === 'string') return c;
|
|
869
|
-
if (Array.isArray(c)) {
|
|
870
|
-
return c.map((p) => (typeof p === 'string' ? p : (p && typeof p === 'object' && typeof p.text === 'string') ? p.text : JSON.stringify(p))).join('\n');
|
|
871
|
-
}
|
|
872
|
-
if (c == null) return '';
|
|
873
|
-
return JSON.stringify(c);
|
|
874
|
-
};
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
Inside the assistant `content.forEach`, after the `thinking` branch, add a `tool_use` branch:
|
|
878
|
-
|
|
879
|
-
```js
|
|
880
|
-
if (c.type === 'tool_use') {
|
|
881
|
-
const callId = String(c.id || `cc-tu-${out.length}`);
|
|
882
|
-
const name = String(c.name || '');
|
|
883
|
-
const input = c.input ?? {};
|
|
884
|
-
|
|
885
|
-
if (name === 'Bash') {
|
|
886
|
-
pending.set(callId, { kind: 'exec' });
|
|
887
|
-
out.push({
|
|
888
|
-
type: 'exec_command_begin',
|
|
889
|
-
turnId: currentTurnId,
|
|
890
|
-
callId,
|
|
891
|
-
command: String(input.command || ''),
|
|
892
|
-
at,
|
|
893
|
-
});
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
// other tools handled in later tasks
|
|
897
|
-
}
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
In the **non-text-user** branch (currently `// Non-text-user (tool_result) handled in a later task. continue;`), replace with full tool_result dispatch:
|
|
901
|
-
|
|
902
|
-
```js
|
|
903
|
-
if (!currentTurnId) continue;
|
|
904
|
-
const content = line.message.content;
|
|
905
|
-
if (!Array.isArray(content)) continue;
|
|
906
|
-
for (const item of content) {
|
|
907
|
-
if (!item || item.type !== 'tool_result') continue;
|
|
908
|
-
const callId = String(item.tool_use_id || `cc-tr-${out.length}`);
|
|
909
|
-
const isError = item.is_error === true;
|
|
910
|
-
const text = toolResultText(item);
|
|
911
|
-
const p = pending.get(callId);
|
|
912
|
-
|
|
913
|
-
if (p?.kind === 'exec') {
|
|
914
|
-
out.push({
|
|
915
|
-
type: 'exec_command_end',
|
|
916
|
-
turnId: currentTurnId,
|
|
917
|
-
callId,
|
|
918
|
-
exit: isError ? 1 : 0,
|
|
919
|
-
stdout: isError ? '' : text,
|
|
920
|
-
stderr: isError ? text : '',
|
|
921
|
-
durationMs: 0,
|
|
922
|
-
at,
|
|
923
|
-
});
|
|
924
|
-
pending.delete(callId);
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
// other kinds handled in later tasks
|
|
928
|
-
}
|
|
929
|
-
continue;
|
|
930
|
-
```
|
|
931
|
-
|
|
932
|
-
- [ ] **Step 4: Run — expect pass**
|
|
933
|
-
|
|
934
|
-
```bash
|
|
935
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
- [ ] **Step 5: Commit**
|
|
939
|
-
|
|
940
|
-
```bash
|
|
941
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
942
|
-
git commit -m "feat(adapter): map Bash tool to exec_command_begin/end"
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
---
|
|
946
|
-
|
|
947
|
-
## Task 11: `TodoWrite` tool → `todo_list`
|
|
948
|
-
|
|
949
|
-
**Files:**
|
|
950
|
-
- Modify: `playground/adapter.mjs`
|
|
951
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
952
|
-
|
|
953
|
-
- [ ] **Step 1: Write the failing test**
|
|
954
|
-
|
|
955
|
-
Append:
|
|
956
|
-
|
|
957
|
-
```js
|
|
958
|
-
describe('adaptClaudeCode · TodoWrite', () => {
|
|
959
|
-
it('emits a todo_list with completed=true only for status==="completed"', () => {
|
|
960
|
-
const lines = [
|
|
961
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
962
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'plan it' } },
|
|
963
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
964
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
965
|
-
message: { role: 'assistant', content: [
|
|
966
|
-
{ type: 'tool_use', id: 'tu-1', name: 'TodoWrite', input: { todos: [
|
|
967
|
-
{ content: 'A', status: 'completed', activeForm: 'Doing A' },
|
|
968
|
-
{ content: 'B', status: 'in_progress', activeForm: 'Doing B' },
|
|
969
|
-
{ content: 'C', status: 'pending', activeForm: 'Doing C' },
|
|
970
|
-
] } },
|
|
971
|
-
] } },
|
|
972
|
-
];
|
|
973
|
-
const { events } = adapt(lines);
|
|
974
|
-
const td = events.find((e) => e.type === 'todo_list');
|
|
975
|
-
expect(td).toMatchObject({
|
|
976
|
-
itemId: 'tu-1',
|
|
977
|
-
turnId: 'u-1',
|
|
978
|
-
items: [
|
|
979
|
-
{ text: 'A', completed: true },
|
|
980
|
-
{ text: 'B', completed: false },
|
|
981
|
-
{ text: 'C', completed: false },
|
|
982
|
-
],
|
|
983
|
-
});
|
|
984
|
-
});
|
|
985
|
-
|
|
986
|
-
it('drops the matching tool_result ack (no extra events)', () => {
|
|
987
|
-
const lines = [
|
|
988
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
989
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
990
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
991
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
992
|
-
message: { role: 'assistant', content: [
|
|
993
|
-
{ type: 'tool_use', id: 'tu-1', name: 'TodoWrite', input: { todos: [] } },
|
|
994
|
-
] } },
|
|
995
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
996
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
997
|
-
message: { role: 'user', content: [
|
|
998
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'ok', is_error: false },
|
|
999
|
-
] } },
|
|
1000
|
-
];
|
|
1001
|
-
const { events } = adapt(lines);
|
|
1002
|
-
// 1 thread_started + 1 turn_started + 1 user_message + 1 todo_list + 1 turn_completed = 5
|
|
1003
|
-
expect(events).toHaveLength(5);
|
|
1004
|
-
});
|
|
1005
|
-
});
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
- [ ] **Step 2: Run — expect failures**
|
|
1009
|
-
|
|
1010
|
-
- [ ] **Step 3: Add TodoWrite branch**
|
|
1011
|
-
|
|
1012
|
-
In the tool_use dispatcher (assistant content branch), after the `Bash` branch, add:
|
|
1013
|
-
|
|
1014
|
-
```js
|
|
1015
|
-
if (name === 'TodoWrite') {
|
|
1016
|
-
pending.set(callId, { kind: 'todo' });
|
|
1017
|
-
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
1018
|
-
out.push({
|
|
1019
|
-
type: 'todo_list',
|
|
1020
|
-
turnId: currentTurnId,
|
|
1021
|
-
itemId: callId,
|
|
1022
|
-
items: todos.map((t) => ({
|
|
1023
|
-
text: String(t?.content || ''),
|
|
1024
|
-
completed: t?.status === 'completed',
|
|
1025
|
-
})),
|
|
1026
|
-
at,
|
|
1027
|
-
});
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
```
|
|
1031
|
-
|
|
1032
|
-
In the tool_result dispatcher (non-text-user branch), after the `exec` branch, add:
|
|
1033
|
-
|
|
1034
|
-
```js
|
|
1035
|
-
if (p?.kind === 'todo') {
|
|
1036
|
-
pending.delete(callId);
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
- [ ] **Step 4: Run — expect pass**
|
|
1042
|
-
|
|
1043
|
-
- [ ] **Step 5: Commit**
|
|
1044
|
-
|
|
1045
|
-
```bash
|
|
1046
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
1047
|
-
git commit -m "feat(adapter): map TodoWrite tool to todo_list"
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
---
|
|
1051
|
-
|
|
1052
|
-
## Task 12: `Edit` → `patch_apply_end` (delayed emit at tool_result)
|
|
1053
|
-
|
|
1054
|
-
**Files:**
|
|
1055
|
-
- Modify: `playground/adapter.mjs`
|
|
1056
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
1057
|
-
|
|
1058
|
-
- [ ] **Step 1: Write the failing test**
|
|
1059
|
-
|
|
1060
|
-
Append:
|
|
1061
|
-
|
|
1062
|
-
```js
|
|
1063
|
-
describe('adaptClaudeCode · Edit', () => {
|
|
1064
|
-
it('synthesises patch_apply_end with -/+ diff on tool_result', () => {
|
|
1065
|
-
const lines = [
|
|
1066
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1067
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1068
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1069
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1070
|
-
message: { role: 'assistant', content: [
|
|
1071
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Edit',
|
|
1072
|
-
input: { file_path: '/p.ts', old_string: 'foo\nbar', new_string: 'baz\nqux' } },
|
|
1073
|
-
] } },
|
|
1074
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1075
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1076
|
-
message: { role: 'user', content: [
|
|
1077
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'edited', is_error: false },
|
|
1078
|
-
] } },
|
|
1079
|
-
];
|
|
1080
|
-
const { events } = adapt(lines);
|
|
1081
|
-
// Edit must NOT emit at tool_use (no early patch_apply_end before tool_result)
|
|
1082
|
-
const idxAssistant = events.findIndex((e) => e.type === 'exec_command_begin' || e.type === 'function_call' || e.type === 'patch_apply_end');
|
|
1083
|
-
const patch = events.find((e) => e.type === 'patch_apply_end');
|
|
1084
|
-
expect(patch).toBeDefined();
|
|
1085
|
-
expect(patch).toMatchObject({
|
|
1086
|
-
callId: 'tu-1',
|
|
1087
|
-
ok: true,
|
|
1088
|
-
files: [{ path: '/p.ts', status: 'modified' }],
|
|
1089
|
-
});
|
|
1090
|
-
expect(patch.files[0].diff).toBe('- foo\n- bar\n+ baz\n+ qux');
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
it('sets ok=false when tool_result.is_error is true', () => {
|
|
1094
|
-
const lines = [
|
|
1095
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1096
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1097
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1098
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1099
|
-
message: { role: 'assistant', content: [
|
|
1100
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Edit',
|
|
1101
|
-
input: { file_path: '/p.ts', old_string: 'x', new_string: 'y' } },
|
|
1102
|
-
] } },
|
|
1103
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1104
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1105
|
-
message: { role: 'user', content: [
|
|
1106
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'not found', is_error: true },
|
|
1107
|
-
] } },
|
|
1108
|
-
];
|
|
1109
|
-
const { events } = adapt(lines);
|
|
1110
|
-
expect(events.find((e) => e.type === 'patch_apply_end').ok).toBe(false);
|
|
1111
|
-
});
|
|
1112
|
-
});
|
|
1113
|
-
```
|
|
1114
|
-
|
|
1115
|
-
- [ ] **Step 2: Run — expect failures**
|
|
1116
|
-
|
|
1117
|
-
- [ ] **Step 3: Add diff helpers near the top of `adapter.mjs` (outside `adaptClaudeCode`)**
|
|
1118
|
-
|
|
1119
|
-
Just before `function adaptClaudeCode(lines) {`, add:
|
|
1120
|
-
|
|
1121
|
-
```js
|
|
1122
|
-
const ccPrefixLines = (text, prefix) => {
|
|
1123
|
-
if (!text) return '';
|
|
1124
|
-
return text.split('\n').map((line) => `${prefix}${line}`).join('\n');
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
const ccEditDiff = (oldStr, newStr) =>
|
|
1128
|
-
`${ccPrefixLines(oldStr, '- ')}\n${ccPrefixLines(newStr, '+ ')}`;
|
|
1129
|
-
|
|
1130
|
-
const ccWriteDiff = (content) => ccPrefixLines(content, '+ ');
|
|
1131
|
-
|
|
1132
|
-
const ccMultiEditDiff = (edits) =>
|
|
1133
|
-
edits.map((e) => `${ccPrefixLines(String(e?.old_string || ''), '- ')}\n${ccPrefixLines(String(e?.new_string || ''), '+ ')}`).join('\n\n');
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
- [ ] **Step 4: Add Edit branch to tool_use dispatcher**
|
|
1137
|
-
|
|
1138
|
-
After the `TodoWrite` branch:
|
|
1139
|
-
|
|
1140
|
-
```js
|
|
1141
|
-
if (name === 'Edit') {
|
|
1142
|
-
pending.set(callId, {
|
|
1143
|
-
kind: 'patch',
|
|
1144
|
-
files: [{
|
|
1145
|
-
path: String(input.file_path || ''),
|
|
1146
|
-
status: 'modified',
|
|
1147
|
-
diff: ccEditDiff(String(input.old_string || ''), String(input.new_string || '')),
|
|
1148
|
-
}],
|
|
1149
|
-
});
|
|
1150
|
-
return; // emission deferred to tool_result so we know `ok`
|
|
1151
|
-
}
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
- [ ] **Step 5: Add patch branch to tool_result dispatcher**
|
|
1155
|
-
|
|
1156
|
-
After the `todo` branch:
|
|
1157
|
-
|
|
1158
|
-
```js
|
|
1159
|
-
if (p?.kind === 'patch') {
|
|
1160
|
-
out.push({
|
|
1161
|
-
type: 'patch_apply_end',
|
|
1162
|
-
turnId: currentTurnId,
|
|
1163
|
-
callId,
|
|
1164
|
-
files: p.files,
|
|
1165
|
-
ok: !isError,
|
|
1166
|
-
at,
|
|
1167
|
-
});
|
|
1168
|
-
pending.delete(callId);
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
```
|
|
1172
|
-
|
|
1173
|
-
- [ ] **Step 6: Run — expect pass**
|
|
1174
|
-
|
|
1175
|
-
```bash
|
|
1176
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
- [ ] **Step 7: Commit**
|
|
1180
|
-
|
|
1181
|
-
```bash
|
|
1182
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
1183
|
-
git commit -m "feat(adapter): map Edit tool to patch_apply_end"
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
---
|
|
1187
|
-
|
|
1188
|
-
## Task 13: `Write` → `patch_apply_end` (status='added')
|
|
1189
|
-
|
|
1190
|
-
**Files:**
|
|
1191
|
-
- Modify: `playground/adapter.mjs`
|
|
1192
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
1193
|
-
|
|
1194
|
-
- [ ] **Step 1: Write the failing test**
|
|
1195
|
-
|
|
1196
|
-
Append:
|
|
1197
|
-
|
|
1198
|
-
```js
|
|
1199
|
-
describe('adaptClaudeCode · Write', () => {
|
|
1200
|
-
it('emits patch_apply_end with status="added" and +-prefixed diff', () => {
|
|
1201
|
-
const lines = [
|
|
1202
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1203
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1204
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1205
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1206
|
-
message: { role: 'assistant', content: [
|
|
1207
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Write',
|
|
1208
|
-
input: { file_path: '/new.ts', content: 'line1\nline2' } },
|
|
1209
|
-
] } },
|
|
1210
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1211
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1212
|
-
message: { role: 'user', content: [
|
|
1213
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'written', is_error: false },
|
|
1214
|
-
] } },
|
|
1215
|
-
];
|
|
1216
|
-
const { events } = adapt(lines);
|
|
1217
|
-
const patch = events.find((e) => e.type === 'patch_apply_end');
|
|
1218
|
-
expect(patch.files[0]).toMatchObject({ path: '/new.ts', status: 'added' });
|
|
1219
|
-
expect(patch.files[0].diff).toBe('+ line1\n+ line2');
|
|
1220
|
-
});
|
|
1221
|
-
});
|
|
1222
|
-
```
|
|
1223
|
-
|
|
1224
|
-
- [ ] **Step 2: Run — expect failure**
|
|
1225
|
-
|
|
1226
|
-
- [ ] **Step 3: Add Write branch**
|
|
1227
|
-
|
|
1228
|
-
After the `Edit` branch:
|
|
1229
|
-
|
|
1230
|
-
```js
|
|
1231
|
-
if (name === 'Write') {
|
|
1232
|
-
pending.set(callId, {
|
|
1233
|
-
kind: 'patch',
|
|
1234
|
-
files: [{
|
|
1235
|
-
path: String(input.file_path || ''),
|
|
1236
|
-
status: 'added',
|
|
1237
|
-
diff: ccWriteDiff(String(input.content || '')),
|
|
1238
|
-
}],
|
|
1239
|
-
});
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
```
|
|
1243
|
-
|
|
1244
|
-
- [ ] **Step 4: Run — expect pass**
|
|
1245
|
-
|
|
1246
|
-
- [ ] **Step 5: Commit**
|
|
1247
|
-
|
|
1248
|
-
```bash
|
|
1249
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
1250
|
-
git commit -m "feat(adapter): map Write tool to patch_apply_end(status=added)"
|
|
1251
|
-
```
|
|
1252
|
-
|
|
1253
|
-
---
|
|
1254
|
-
|
|
1255
|
-
## Task 14: `MultiEdit` → `patch_apply_end` with multiple -/+ blocks
|
|
1256
|
-
|
|
1257
|
-
**Files:**
|
|
1258
|
-
- Modify: `playground/adapter.mjs`
|
|
1259
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
1260
|
-
|
|
1261
|
-
- [ ] **Step 1: Write the failing test**
|
|
1262
|
-
|
|
1263
|
-
Append:
|
|
1264
|
-
|
|
1265
|
-
```js
|
|
1266
|
-
describe('adaptClaudeCode · MultiEdit', () => {
|
|
1267
|
-
it('joins multiple edits with blank lines and labels them as modified', () => {
|
|
1268
|
-
const lines = [
|
|
1269
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1270
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1271
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1272
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1273
|
-
message: { role: 'assistant', content: [
|
|
1274
|
-
{ type: 'tool_use', id: 'tu-1', name: 'MultiEdit',
|
|
1275
|
-
input: { file_path: '/p.ts', edits: [
|
|
1276
|
-
{ old_string: 'a', new_string: 'A' },
|
|
1277
|
-
{ old_string: 'b', new_string: 'B' },
|
|
1278
|
-
] } },
|
|
1279
|
-
] } },
|
|
1280
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1281
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1282
|
-
message: { role: 'user', content: [
|
|
1283
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'done', is_error: false },
|
|
1284
|
-
] } },
|
|
1285
|
-
];
|
|
1286
|
-
const { events } = adapt(lines);
|
|
1287
|
-
const patch = events.find((e) => e.type === 'patch_apply_end');
|
|
1288
|
-
expect(patch.files[0].status).toBe('modified');
|
|
1289
|
-
expect(patch.files[0].diff).toBe('- a\n+ A\n\n- b\n+ B');
|
|
1290
|
-
});
|
|
1291
|
-
});
|
|
1292
|
-
```
|
|
1293
|
-
|
|
1294
|
-
- [ ] **Step 2: Run — expect failure**
|
|
1295
|
-
|
|
1296
|
-
- [ ] **Step 3: Add MultiEdit branch**
|
|
1297
|
-
|
|
1298
|
-
After the `Write` branch:
|
|
1299
|
-
|
|
1300
|
-
```js
|
|
1301
|
-
if (name === 'MultiEdit') {
|
|
1302
|
-
const edits = Array.isArray(input.edits) ? input.edits : [];
|
|
1303
|
-
pending.set(callId, {
|
|
1304
|
-
kind: 'patch',
|
|
1305
|
-
files: [{
|
|
1306
|
-
path: String(input.file_path || ''),
|
|
1307
|
-
status: 'modified',
|
|
1308
|
-
diff: ccMultiEditDiff(edits),
|
|
1309
|
-
}],
|
|
1310
|
-
});
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
```
|
|
1314
|
-
|
|
1315
|
-
- [ ] **Step 4: Run — expect pass**
|
|
1316
|
-
|
|
1317
|
-
- [ ] **Step 5: Commit**
|
|
1318
|
-
|
|
1319
|
-
```bash
|
|
1320
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
1321
|
-
git commit -m "feat(adapter): map MultiEdit tool to patch_apply_end"
|
|
1322
|
-
```
|
|
1323
|
-
|
|
1324
|
-
---
|
|
1325
|
-
|
|
1326
|
-
## Task 15: MCP tools (`mcp__server__name`) → `mcp_tool_call` + `mcp_tool_call_output`
|
|
1327
|
-
|
|
1328
|
-
**Files:**
|
|
1329
|
-
- Modify: `playground/adapter.mjs`
|
|
1330
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
1331
|
-
|
|
1332
|
-
- [ ] **Step 1: Write the failing test**
|
|
1333
|
-
|
|
1334
|
-
Append:
|
|
1335
|
-
|
|
1336
|
-
```js
|
|
1337
|
-
describe('adaptClaudeCode · MCP', () => {
|
|
1338
|
-
it('parses mcp__server__name into server + name', () => {
|
|
1339
|
-
const lines = [
|
|
1340
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1341
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1342
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1343
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1344
|
-
message: { role: 'assistant', content: [
|
|
1345
|
-
{ type: 'tool_use', id: 'tu-1', name: 'mcp__weather__get_forecast', input: { city: 'AKL' } },
|
|
1346
|
-
] } },
|
|
1347
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1348
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1349
|
-
message: { role: 'user', content: [
|
|
1350
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'rain', is_error: false },
|
|
1351
|
-
] } },
|
|
1352
|
-
];
|
|
1353
|
-
const { events } = adapt(lines);
|
|
1354
|
-
const call = events.find((e) => e.type === 'mcp_tool_call');
|
|
1355
|
-
const output = events.find((e) => e.type === 'mcp_tool_call_output');
|
|
1356
|
-
expect(call).toMatchObject({ callId: 'tu-1', server: 'weather', name: 'get_forecast', args: { city: 'AKL' } });
|
|
1357
|
-
expect(output).toMatchObject({ callId: 'tu-1', output: 'rain' });
|
|
1358
|
-
expect(output.error).toBeUndefined();
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
it('handles names with three+ segments by treating extras as part of name', () => {
|
|
1362
|
-
const lines = [
|
|
1363
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1364
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1365
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1366
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1367
|
-
message: { role: 'assistant', content: [
|
|
1368
|
-
{ type: 'tool_use', id: 'tu-1', name: 'mcp__foo__bar__baz', input: {} },
|
|
1369
|
-
] } },
|
|
1370
|
-
];
|
|
1371
|
-
const { events } = adapt(lines);
|
|
1372
|
-
const call = events.find((e) => e.type === 'mcp_tool_call');
|
|
1373
|
-
expect(call).toMatchObject({ server: 'foo', name: 'bar__baz' });
|
|
1374
|
-
});
|
|
1375
|
-
|
|
1376
|
-
it('routes tool_result with is_error=true into mcp_tool_call_output.error', () => {
|
|
1377
|
-
const lines = [
|
|
1378
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1379
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1380
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1381
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1382
|
-
message: { role: 'assistant', content: [
|
|
1383
|
-
{ type: 'tool_use', id: 'tu-1', name: 'mcp__svc__op', input: {} },
|
|
1384
|
-
] } },
|
|
1385
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1386
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1387
|
-
message: { role: 'user', content: [
|
|
1388
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'boom', is_error: true },
|
|
1389
|
-
] } },
|
|
1390
|
-
];
|
|
1391
|
-
const { events } = adapt(lines);
|
|
1392
|
-
const output = events.find((e) => e.type === 'mcp_tool_call_output');
|
|
1393
|
-
expect(output).toMatchObject({ error: 'boom' });
|
|
1394
|
-
expect(output.output).toBeUndefined();
|
|
1395
|
-
});
|
|
1396
|
-
});
|
|
1397
|
-
```
|
|
1398
|
-
|
|
1399
|
-
- [ ] **Step 2: Run — expect failures**
|
|
1400
|
-
|
|
1401
|
-
- [ ] **Step 3: Add MCP branch (tool_use dispatcher)**
|
|
1402
|
-
|
|
1403
|
-
After the `MultiEdit` branch:
|
|
1404
|
-
|
|
1405
|
-
```js
|
|
1406
|
-
if (name.startsWith('mcp__')) {
|
|
1407
|
-
const parts = name.split('__');
|
|
1408
|
-
const server = parts[1] || '';
|
|
1409
|
-
const toolName = parts.slice(2).join('__') || name;
|
|
1410
|
-
pending.set(callId, { kind: 'mcp' });
|
|
1411
|
-
out.push({
|
|
1412
|
-
type: 'mcp_tool_call',
|
|
1413
|
-
turnId: currentTurnId,
|
|
1414
|
-
callId,
|
|
1415
|
-
server,
|
|
1416
|
-
name: toolName,
|
|
1417
|
-
args: input,
|
|
1418
|
-
at,
|
|
1419
|
-
});
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
```
|
|
1423
|
-
|
|
1424
|
-
- [ ] **Step 4: Add MCP branch (tool_result dispatcher)**
|
|
1425
|
-
|
|
1426
|
-
After the `patch` branch:
|
|
1427
|
-
|
|
1428
|
-
```js
|
|
1429
|
-
if (p?.kind === 'mcp') {
|
|
1430
|
-
const evt = { type: 'mcp_tool_call_output', turnId: currentTurnId, callId, at };
|
|
1431
|
-
if (isError) evt.error = text; else evt.output = text;
|
|
1432
|
-
out.push(evt);
|
|
1433
|
-
pending.delete(callId);
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
```
|
|
1437
|
-
|
|
1438
|
-
- [ ] **Step 5: Run — expect pass**
|
|
1439
|
-
|
|
1440
|
-
- [ ] **Step 6: Commit**
|
|
1441
|
-
|
|
1442
|
-
```bash
|
|
1443
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
1444
|
-
git commit -m "feat(adapter): map mcp__server__name tools to mcp_tool_call(_output)"
|
|
1445
|
-
```
|
|
1446
|
-
|
|
1447
|
-
---
|
|
1448
|
-
|
|
1449
|
-
## Task 16: Generic tool fallback → `function_call` + `function_call_output`
|
|
1450
|
-
|
|
1451
|
-
**Files:**
|
|
1452
|
-
- Modify: `playground/adapter.mjs`
|
|
1453
|
-
- Modify: `playground/adapter.claude-code.test.mjs`
|
|
1454
|
-
|
|
1455
|
-
- [ ] **Step 1: Write the failing test**
|
|
1456
|
-
|
|
1457
|
-
Append:
|
|
1458
|
-
|
|
1459
|
-
```js
|
|
1460
|
-
describe('adaptClaudeCode · function_call fallback', () => {
|
|
1461
|
-
it('maps unknown tools (e.g. Read) to function_call', () => {
|
|
1462
|
-
const lines = [
|
|
1463
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1464
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1465
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1466
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1467
|
-
message: { role: 'assistant', content: [
|
|
1468
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Read', input: { file_path: '/x' } },
|
|
1469
|
-
] } },
|
|
1470
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1471
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1472
|
-
message: { role: 'user', content: [
|
|
1473
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'contents', is_error: false },
|
|
1474
|
-
] } },
|
|
1475
|
-
];
|
|
1476
|
-
const { events } = adapt(lines);
|
|
1477
|
-
const call = events.find((e) => e.type === 'function_call');
|
|
1478
|
-
const out_ = events.find((e) => e.type === 'function_call_output');
|
|
1479
|
-
expect(call).toMatchObject({ callId: 'tu-1', name: 'Read', args: { file_path: '/x' } });
|
|
1480
|
-
expect(out_).toMatchObject({ callId: 'tu-1', output: 'contents' });
|
|
1481
|
-
});
|
|
1482
|
-
|
|
1483
|
-
it('routes is_error into function_call_output.error', () => {
|
|
1484
|
-
const lines = [
|
|
1485
|
-
{ type: 'user', uuid: 'u-1', sessionId: 'sess', parentUuid: null,
|
|
1486
|
-
timestamp: '2026-05-15T00:00:00Z', message: { role: 'user', content: 'q' } },
|
|
1487
|
-
{ type: 'assistant', uuid: 'a-1', sessionId: 'sess', parentUuid: 'u-1',
|
|
1488
|
-
timestamp: '2026-05-15T00:00:01Z',
|
|
1489
|
-
message: { role: 'assistant', content: [
|
|
1490
|
-
{ type: 'tool_use', id: 'tu-1', name: 'Glob', input: { pattern: '**/*.ts' } },
|
|
1491
|
-
] } },
|
|
1492
|
-
{ type: 'user', uuid: 'u-2', sessionId: 'sess', parentUuid: 'a-1',
|
|
1493
|
-
timestamp: '2026-05-15T00:00:02Z',
|
|
1494
|
-
message: { role: 'user', content: [
|
|
1495
|
-
{ type: 'tool_result', tool_use_id: 'tu-1', content: 'bad pattern', is_error: true },
|
|
1496
|
-
] } },
|
|
1497
|
-
];
|
|
1498
|
-
const { events } = adapt(lines);
|
|
1499
|
-
const out_ = events.find((e) => e.type === 'function_call_output');
|
|
1500
|
-
expect(out_).toMatchObject({ error: 'bad pattern' });
|
|
1501
|
-
expect(out_.output).toBeUndefined();
|
|
1502
|
-
});
|
|
1503
|
-
});
|
|
1504
|
-
```
|
|
1505
|
-
|
|
1506
|
-
- [ ] **Step 2: Run — expect failures**
|
|
1507
|
-
|
|
1508
|
-
- [ ] **Step 3: Add default branch in tool_use dispatcher**
|
|
1509
|
-
|
|
1510
|
-
After the MCP branch (at the end of the `if (c.type === 'tool_use') {…}` block):
|
|
1511
|
-
|
|
1512
|
-
```js
|
|
1513
|
-
// Fallback: any other tool name (Read, Glob, Grep, Task, Skill, WebSearch, WebFetch, …)
|
|
1514
|
-
pending.set(callId, { kind: 'function' });
|
|
1515
|
-
out.push({
|
|
1516
|
-
type: 'function_call',
|
|
1517
|
-
turnId: currentTurnId,
|
|
1518
|
-
callId,
|
|
1519
|
-
name,
|
|
1520
|
-
args: input,
|
|
1521
|
-
at,
|
|
1522
|
-
});
|
|
1523
|
-
return;
|
|
1524
|
-
```
|
|
1525
|
-
|
|
1526
|
-
- [ ] **Step 4: Add default branch in tool_result dispatcher**
|
|
1527
|
-
|
|
1528
|
-
After the MCP branch:
|
|
1529
|
-
|
|
1530
|
-
```js
|
|
1531
|
-
if (p?.kind === 'function') {
|
|
1532
|
-
const evt = { type: 'function_call_output', turnId: currentTurnId, callId, at };
|
|
1533
|
-
if (isError) evt.error = text; else evt.output = text;
|
|
1534
|
-
out.push(evt);
|
|
1535
|
-
pending.delete(callId);
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
// Unknown / orphan tool_result with no matching tool_use — surface as a function_call_output
|
|
1539
|
-
// so the user can at least see it in the transcript.
|
|
1540
|
-
const evt = { type: 'function_call_output', turnId: currentTurnId, callId, at };
|
|
1541
|
-
if (isError) evt.error = text; else evt.output = text;
|
|
1542
|
-
out.push(evt);
|
|
1543
|
-
```
|
|
1544
|
-
|
|
1545
|
-
- [ ] **Step 5: Run — expect pass**
|
|
1546
|
-
|
|
1547
|
-
```bash
|
|
1548
|
-
pnpm test playground/adapter.claude-code.test.mjs
|
|
1549
|
-
```
|
|
1550
|
-
|
|
1551
|
-
- [ ] **Step 6: Run the full test suite to make sure existing Codex tests still pass**
|
|
1552
|
-
|
|
1553
|
-
```bash
|
|
1554
|
-
pnpm test
|
|
1555
|
-
```
|
|
1556
|
-
Expected: All green.
|
|
1557
|
-
|
|
1558
|
-
- [ ] **Step 7: Commit**
|
|
1559
|
-
|
|
1560
|
-
```bash
|
|
1561
|
-
git add playground/adapter.mjs playground/adapter.claude-code.test.mjs
|
|
1562
|
-
git commit -m "feat(adapter): fallback unknown tools to function_call"
|
|
1563
|
-
```
|
|
1564
|
-
|
|
1565
|
-
---
|
|
1566
|
-
|
|
1567
|
-
## Task 17: `playground/api.mjs` — scan `~/.claude/projects/`
|
|
1568
|
-
|
|
1569
|
-
**Files:**
|
|
1570
|
-
- Modify: `playground/api.mjs`
|
|
1571
|
-
|
|
1572
|
-
- [ ] **Step 1: Read the current `playground/api.mjs:11-22`**
|
|
1573
|
-
|
|
1574
|
-
Verify the current top-of-file imports and `HOME` / `ROLLOUT_ROOT` / `TEAM_ROOT` constants.
|
|
1575
|
-
|
|
1576
|
-
- [ ] **Step 2: Add `CLAUDE_ROOT` constant**
|
|
1577
|
-
|
|
1578
|
-
After `const TEAM_ROOT = join(HOME, 'Projects/agentweb/.codex-team/runs');`, add:
|
|
1579
|
-
|
|
1580
|
-
```js
|
|
1581
|
-
const CLAUDE_ROOT = join(HOME, '.claude/projects');
|
|
1582
|
-
```
|
|
1583
|
-
|
|
1584
|
-
- [ ] **Step 3: Add a third scan block in `listFiles()`**
|
|
1585
|
-
|
|
1586
|
-
After the AgentWeb codex-team block (inside `listFiles()`, before the `out.sort(...)` line), add:
|
|
1587
|
-
|
|
1588
|
-
```js
|
|
1589
|
-
// Claude Code session JSONL (main session only; subagents/ excluded by maxdepth + path filter)
|
|
1590
|
-
try {
|
|
1591
|
-
const stdout = execFileSync(
|
|
1592
|
-
'find',
|
|
1593
|
-
[CLAUDE_ROOT, '-maxdepth', '2', '-name', '*.jsonl', '-type', 'f'],
|
|
1594
|
-
{ encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 },
|
|
1595
|
-
);
|
|
1596
|
-
for (const path of stdout.split('\n').filter(Boolean)) {
|
|
1597
|
-
if (path.includes('/subagents/')) continue;
|
|
1598
|
-
try {
|
|
1599
|
-
const st = statSync(path);
|
|
1600
|
-
const segments = path.split('/');
|
|
1601
|
-
const filename = segments[segments.length - 1] || '';
|
|
1602
|
-
const parentDir = segments[segments.length - 2] || '';
|
|
1603
|
-
// e.g. "a7d93eaf · projects-CodexView"
|
|
1604
|
-
const sessionPrefix = filename.replace(/\.jsonl$/, '').slice(0, 8);
|
|
1605
|
-
const projectTail = parentDir.replace(/^-+/, '').slice(-30);
|
|
1606
|
-
out.push({
|
|
1607
|
-
path,
|
|
1608
|
-
source: 'claude-code',
|
|
1609
|
-
name: `${sessionPrefix} · ${projectTail}`,
|
|
1610
|
-
mtime: Math.floor(st.mtimeMs),
|
|
1611
|
-
sizeKB: Math.round(st.size / 102.4) / 10,
|
|
1612
|
-
});
|
|
1613
|
-
} catch { /* skip unreadable */ }
|
|
1614
|
-
}
|
|
1615
|
-
} catch { /* CLAUDE_ROOT absent — fine */ }
|
|
1616
|
-
```
|
|
1617
|
-
|
|
1618
|
-
- [ ] **Step 4: Update `isAllowed()` to permit `CLAUDE_ROOT` paths**
|
|
1619
|
-
|
|
1620
|
-
Replace:
|
|
1621
|
-
|
|
1622
|
-
```js
|
|
1623
|
-
function isAllowed(path) {
|
|
1624
|
-
return path && (path.startsWith(ROLLOUT_ROOT) || path.startsWith(TEAM_ROOT));
|
|
1625
|
-
}
|
|
1626
|
-
```
|
|
1627
|
-
|
|
1628
|
-
With:
|
|
1629
|
-
|
|
1630
|
-
```js
|
|
1631
|
-
function isAllowed(path) {
|
|
1632
|
-
return path && (
|
|
1633
|
-
path.startsWith(ROLLOUT_ROOT) ||
|
|
1634
|
-
path.startsWith(TEAM_ROOT) ||
|
|
1635
|
-
path.startsWith(CLAUDE_ROOT)
|
|
1636
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
```
|
|
1639
|
-
|
|
1640
|
-
- [ ] **Step 5: Smoke test — start playground and curl the API**
|
|
1641
|
-
|
|
1642
|
-
In one terminal:
|
|
1643
|
-
|
|
1644
|
-
```bash
|
|
1645
|
-
pnpm playground
|
|
1646
|
-
```
|
|
1647
|
-
|
|
1648
|
-
In another:
|
|
1649
|
-
|
|
1650
|
-
```bash
|
|
1651
|
-
curl -s http://127.0.0.1:5181/api/logs | python3 -c "import sys, json; data = json.load(sys.stdin); print('total:', len(data['files'])); from collections import Counter; print(Counter(f['source'] for f in data['files']))"
|
|
1652
|
-
```
|
|
1653
|
-
|
|
1654
|
-
Expected: total > 0, the Counter includes `'claude-code': N` (N > 0 on a machine with prior Claude Code usage).
|
|
1655
|
-
|
|
1656
|
-
If you have at least one Claude Code session, also:
|
|
1657
|
-
|
|
1658
|
-
```bash
|
|
1659
|
-
ID=$(curl -s http://127.0.0.1:5181/api/logs | python3 -c "import sys,json; print(next(f['id'] for f in json.load(sys.stdin)['files'] if f['source']=='claude-code'))")
|
|
1660
|
-
curl -s "http://127.0.0.1:5181/api/logs/$ID/events" | python3 -c "import sys,json; d=json.load(sys.stdin); print('format:', d['format']); print('event types:', sorted({e['type'] for e in d['events']}))"
|
|
1661
|
-
```
|
|
1662
|
-
|
|
1663
|
-
Expected: `format: claude-code`, event types include `thread_started`, `turn_started`, `user_message`, `agent_message`, etc.
|
|
1664
|
-
|
|
1665
|
-
Stop the playground (Ctrl-C in its terminal).
|
|
1666
|
-
|
|
1667
|
-
- [ ] **Step 6: Commit**
|
|
1668
|
-
|
|
1669
|
-
```bash
|
|
1670
|
-
git add playground/api.mjs
|
|
1671
|
-
git commit -m "feat(playground): scan ~/.claude/projects/ as a third source"
|
|
1672
|
-
```
|
|
1673
|
-
|
|
1674
|
-
---
|
|
1675
|
-
|
|
1676
|
-
## Task 18: `playground/src/App.tsx` — add `claude-code` filter & badge
|
|
1677
|
-
|
|
1678
|
-
**Files:**
|
|
1679
|
-
- Modify: `playground/src/App.tsx`
|
|
1680
|
-
|
|
1681
|
-
- [ ] **Step 1: Update `FileEntry.source` type union**
|
|
1682
|
-
|
|
1683
|
-
Find the `interface FileEntry` block at the top of `App.tsx` and replace the `source` field:
|
|
1684
|
-
|
|
1685
|
-
```ts
|
|
1686
|
-
source: 'codex-cli' | 'agentweb-team' | 'claude-code' | 'synthetic';
|
|
1687
|
-
```
|
|
1688
|
-
|
|
1689
|
-
- [ ] **Step 2: Update `EventsResponse.format` type union**
|
|
1690
|
-
|
|
1691
|
-
```ts
|
|
1692
|
-
format: 'rollout' | 'codex-team' | 'claude-code' | 'synthetic' | 'unknown';
|
|
1693
|
-
```
|
|
1694
|
-
|
|
1695
|
-
- [ ] **Step 3: Update the filter `useState` and dropdown**
|
|
1696
|
-
|
|
1697
|
-
Replace the `useState<'all' | 'codex-cli' | 'agentweb-team'>('all')` declaration:
|
|
1698
|
-
|
|
1699
|
-
```ts
|
|
1700
|
-
const [filter, setFilter] = useState<'all' | 'codex-cli' | 'agentweb-team' | 'claude-code'>('all');
|
|
1701
|
-
```
|
|
1702
|
-
|
|
1703
|
-
In the `<select>` below the search input, add a new `<option>` after the existing two:
|
|
1704
|
-
|
|
1705
|
-
```tsx
|
|
1706
|
-
<option value="claude-code">Claude Code ({files.filter((f) => f.source === 'claude-code').length})</option>
|
|
1707
|
-
```
|
|
1708
|
-
|
|
1709
|
-
- [ ] **Step 4: Extend the badge label and color in `styles.badge`**
|
|
1710
|
-
|
|
1711
|
-
Find the JSX that renders the badge (around `<span style={styles.badge} data-source={f.source}>`). Replace with:
|
|
1712
|
-
|
|
1713
|
-
```tsx
|
|
1714
|
-
<span style={styles.badge} data-source={f.source}>
|
|
1715
|
-
{f.source === 'codex-cli' ? 'CLI'
|
|
1716
|
-
: f.source === 'agentweb-team' ? 'Team'
|
|
1717
|
-
: f.source === 'claude-code' ? 'Claude'
|
|
1718
|
-
: 'Demo'}
|
|
1719
|
-
</span>
|
|
1720
|
-
```
|
|
1721
|
-
|
|
1722
|
-
(The `'synthetic'` case maps to `'Demo'` to match the existing synthetic demo entry.)
|
|
1723
|
-
|
|
1724
|
-
- [ ] **Step 5: Add a per-source background colour for the badge**
|
|
1725
|
-
|
|
1726
|
-
Locate the `badge` style in the `styles` object near the bottom of the file. Replace the existing `badge: { … }` block with:
|
|
1727
|
-
|
|
1728
|
-
```ts
|
|
1729
|
-
badge: {
|
|
1730
|
-
padding: '1px 6px',
|
|
1731
|
-
borderRadius: 3,
|
|
1732
|
-
fontSize: 10,
|
|
1733
|
-
background: '#eef2f5',
|
|
1734
|
-
},
|
|
1735
|
-
badgeClaude: { background: '#f4e3ff', color: '#6b21a8' },
|
|
1736
|
-
badgeCli: { background: '#e0f2fe', color: '#075985' },
|
|
1737
|
-
badgeTeam: { background: '#fef3c7', color: '#92400e' },
|
|
1738
|
-
badgeDemo: { background: '#f3f4f6', color: '#4b5563' },
|
|
1739
|
-
```
|
|
1740
|
-
|
|
1741
|
-
Then update the badge JSX from Step 4 to merge styles:
|
|
1742
|
-
|
|
1743
|
-
```tsx
|
|
1744
|
-
<span
|
|
1745
|
-
style={{
|
|
1746
|
-
...styles.badge,
|
|
1747
|
-
...(f.source === 'codex-cli' ? styles.badgeCli
|
|
1748
|
-
: f.source === 'agentweb-team' ? styles.badgeTeam
|
|
1749
|
-
: f.source === 'claude-code' ? styles.badgeClaude
|
|
1750
|
-
: styles.badgeDemo),
|
|
1751
|
-
}}
|
|
1752
|
-
data-source={f.source}
|
|
1753
|
-
>
|
|
1754
|
-
{f.source === 'codex-cli' ? 'CLI'
|
|
1755
|
-
: f.source === 'agentweb-team' ? 'Team'
|
|
1756
|
-
: f.source === 'claude-code' ? 'Claude'
|
|
1757
|
-
: 'Demo'}
|
|
1758
|
-
</span>
|
|
1759
|
-
```
|
|
1760
|
-
|
|
1761
|
-
- [ ] **Step 6: Typecheck**
|
|
1762
|
-
|
|
1763
|
-
```bash
|
|
1764
|
-
pnpm typecheck
|
|
1765
|
-
```
|
|
1766
|
-
Expected: 0 errors.
|
|
1767
|
-
|
|
1768
|
-
- [ ] **Step 7: Visual smoke**
|
|
1769
|
-
|
|
1770
|
-
Start playground:
|
|
1771
|
-
|
|
1772
|
-
```bash
|
|
1773
|
-
pnpm playground
|
|
1774
|
-
```
|
|
1775
|
-
|
|
1776
|
-
Open http://127.0.0.1:5181 in a browser:
|
|
1777
|
-
- Confirm filter dropdown shows "Claude Code (N)"
|
|
1778
|
-
- Confirm a `Claude` badge with purple background appears next to Claude Code entries
|
|
1779
|
-
- Click a `Claude Code` file → confirm `format: claude-code` in the top meta row
|
|
1780
|
-
- Confirm transcript renders without console errors
|
|
1781
|
-
|
|
1782
|
-
Stop the playground (Ctrl-C).
|
|
1783
|
-
|
|
1784
|
-
- [ ] **Step 8: Commit**
|
|
1785
|
-
|
|
1786
|
-
```bash
|
|
1787
|
-
git add playground/src/App.tsx
|
|
1788
|
-
git commit -m "feat(playground): show claude-code source in filter and badge"
|
|
1789
|
-
```
|
|
1790
|
-
|
|
1791
|
-
---
|
|
1792
|
-
|
|
1793
|
-
## Task 19: Add real-derived anonymized fixtures
|
|
1794
|
-
|
|
1795
|
-
**Files:**
|
|
1796
|
-
- Create: `fixtures/claude-code/short.jsonl`
|
|
1797
|
-
- Create: `fixtures/claude-code/tool-heavy.jsonl`
|
|
1798
|
-
- Create: `fixtures/claude-code/thinking-mixed.jsonl`
|
|
1799
|
-
- Modify: `fixtures/README.md`
|
|
1800
|
-
|
|
1801
|
-
These fixtures are **raw Claude Code JSONL** (NOT adapted `ChatStreamEvent`). They live in a subfolder so they aren't accidentally picked up by `loadFixture.ts` (which expects `ChatStreamEvent` shape in `fixtures/*.jsonl` directly).
|
|
1802
|
-
|
|
1803
|
-
- [ ] **Step 1: Choose three real source sessions**
|
|
1804
|
-
|
|
1805
|
-
Run:
|
|
1806
|
-
|
|
1807
|
-
```bash
|
|
1808
|
-
find ~/.claude/projects -maxdepth 2 -name '*.jsonl' -size +5k -size -200k -not -path '*/subagents/*' 2>/dev/null | head -10
|
|
1809
|
-
```
|
|
1810
|
-
|
|
1811
|
-
Pick three that collectively cover:
|
|
1812
|
-
- A small one (≤20 lines) with one Bash and one assistant text reply → `short.jsonl`
|
|
1813
|
-
- A medium one with Edit / Write or MultiEdit / TodoWrite / at least one `mcp__*` call → `tool-heavy.jsonl`
|
|
1814
|
-
- One containing **both** plaintext thinking AND empty-string thinking → `thinking-mixed.jsonl`. To find one:
|
|
1815
|
-
|
|
1816
|
-
```bash
|
|
1817
|
-
for f in $(find ~/.claude/projects -maxdepth 2 -name '*.jsonl' -not -path '*/subagents/*' 2>/dev/null); do
|
|
1818
|
-
has_plain=$(jq -r 'select(.message.content[]?.type=="thinking" and .message.content[]?.thinking != "") | .uuid' "$f" 2>/dev/null | head -1)
|
|
1819
|
-
has_empty=$(jq -r 'select(.message.content[]?.type=="thinking" and .message.content[]?.thinking == "") | .uuid' "$f" 2>/dev/null | head -1)
|
|
1820
|
-
[ -n "$has_plain" ] && [ -n "$has_empty" ] && { echo "$f"; break; }
|
|
1821
|
-
done
|
|
1822
|
-
```
|
|
1823
|
-
|
|
1824
|
-
- [ ] **Step 2: For each source, hand-pick a 20–60 line slice**
|
|
1825
|
-
|
|
1826
|
-
Use `jq -c` to flatten one line per object, then manually trim with a text editor to the smallest contiguous slice that demonstrates the case. Keep ordering intact (the adapter relies on file order).
|
|
1827
|
-
|
|
1828
|
-
- [ ] **Step 3: Anonymize each fixture**
|
|
1829
|
-
|
|
1830
|
-
Apply these replacements to all three fixtures:
|
|
1831
|
-
|
|
1832
|
-
| Match | Replace with |
|
|
1833
|
-
|---|---|
|
|
1834
|
-
| `/Users/<username>` (any username) | `/Users/<user>` |
|
|
1835
|
-
| Hostnames in URLs (anything not anthropic.com/codexview/github.com/react.dev/openai.com) | `example.com` |
|
|
1836
|
-
| Email addresses | `user@example.com` |
|
|
1837
|
-
| API tokens / Bearer keys / `sk-…` / `npm_…` | `<redacted>` |
|
|
1838
|
-
| Real `sessionId` / `uuid` / `parentUuid` | leave as-is (already random) |
|
|
1839
|
-
|
|
1840
|
-
You can do the path replacement with sed; for the more selective stuff, hand-edit:
|
|
1841
|
-
|
|
1842
|
-
```bash
|
|
1843
|
-
sed -i.bak -E 's|/Users/[^/"]+|/Users/<user>|g; s|(npm|sk)[-_][A-Za-z0-9_-]{20,}|<redacted>|g' fixtures/claude-code/*.jsonl
|
|
1844
|
-
rm fixtures/claude-code/*.bak
|
|
1845
|
-
```
|
|
1846
|
-
|
|
1847
|
-
- [ ] **Step 4: Save the three fixtures**
|
|
1848
|
-
|
|
1849
|
-
```
|
|
1850
|
-
fixtures/claude-code/short.jsonl
|
|
1851
|
-
fixtures/claude-code/tool-heavy.jsonl
|
|
1852
|
-
fixtures/claude-code/thinking-mixed.jsonl
|
|
1853
|
-
```
|
|
1854
|
-
|
|
1855
|
-
- [ ] **Step 5: Sanity-check each fixture goes through `adapt()` without errors**
|
|
1856
|
-
|
|
1857
|
-
Run a small one-off check:
|
|
1858
|
-
|
|
1859
|
-
```bash
|
|
1860
|
-
node --input-type=module -e "
|
|
1861
|
-
import { readFileSync } from 'node:fs';
|
|
1862
|
-
import { adapt, parseJsonl } from './playground/adapter.mjs';
|
|
1863
|
-
for (const name of ['short', 'tool-heavy', 'thinking-mixed']) {
|
|
1864
|
-
const path = 'fixtures/claude-code/' + name + '.jsonl';
|
|
1865
|
-
const lines = parseJsonl(readFileSync(path, 'utf8'));
|
|
1866
|
-
const { format, events } = adapt(lines);
|
|
1867
|
-
console.log(name, 'format=' + format, 'events=' + events.length, 'types=' + JSON.stringify([...new Set(events.map(e => e.type))].sort()));
|
|
1868
|
-
}
|
|
1869
|
-
"
|
|
1870
|
-
```
|
|
1871
|
-
|
|
1872
|
-
Expected:
|
|
1873
|
-
- `format=claude-code` for all three
|
|
1874
|
-
- short.jsonl events include at least `thread_started`, `turn_started`, `user_message`, `exec_command_begin`, `exec_command_end`, `agent_message`, `turn_completed`
|
|
1875
|
-
- tool-heavy.jsonl events include at least `patch_apply_end`, `todo_list`, `mcp_tool_call`
|
|
1876
|
-
- thinking-mixed.jsonl events include `reasoning` (the count of `reasoning` events must equal the count of `thinking` blocks with non-empty text)
|
|
1877
|
-
|
|
1878
|
-
If any fixture fails the sanity check, fix the slice (likely a truncation issue) and retry.
|
|
1879
|
-
|
|
1880
|
-
- [ ] **Step 6: Append to `fixtures/README.md`**
|
|
1881
|
-
|
|
1882
|
-
At the bottom of `fixtures/README.md`, add:
|
|
1883
|
-
|
|
1884
|
-
```markdown
|
|
1885
|
-
## Claude Code raw fixtures (`claude-code/`)
|
|
1886
|
-
|
|
1887
|
-
The files in `fixtures/claude-code/` are **raw Claude Code session JSONL** —
|
|
1888
|
-
each line is exactly the shape Claude Code writes to
|
|
1889
|
-
`~/.claude/projects/<repo>/<sessionId>.jsonl`. They are NOT
|
|
1890
|
-
`ChatStreamEvent` and are NOT consumed by `loadFixture.ts`. They feed
|
|
1891
|
-
`playground/adapter.claude-code.test.mjs` and the playground SPA.
|
|
1892
|
-
|
|
1893
|
-
Anonymization rules:
|
|
1894
|
-
- Usernames in absolute paths → `<user>`
|
|
1895
|
-
- Third-party hostnames (except anthropic.com, codexview.*, github.com, react.dev) → `example.com`
|
|
1896
|
-
- Email addresses → `user@example.com`
|
|
1897
|
-
- API tokens / Bearer keys / `sk-…` / `npm_…` → `<redacted>`
|
|
1898
|
-
- `sessionId`/`uuid`/`parentUuid` are kept (already random)
|
|
1899
|
-
|
|
1900
|
-
Inventory:
|
|
1901
|
-
- `short.jsonl` — single turn with 1 Bash + 1 assistant message
|
|
1902
|
-
- `tool-heavy.jsonl` — multi-turn: Edit/MultiEdit/Bash/TodoWrite/mcp_tool/WebSearch
|
|
1903
|
-
- `thinking-mixed.jsonl` — plaintext thinking + empty (encrypted) thinking
|
|
1904
|
-
```
|
|
1905
|
-
|
|
1906
|
-
- [ ] **Step 7: Commit**
|
|
1907
|
-
|
|
1908
|
-
```bash
|
|
1909
|
-
git add fixtures/claude-code/ fixtures/README.md
|
|
1910
|
-
git commit -m "test: add anonymized Claude Code raw JSONL fixtures"
|
|
1911
|
-
```
|
|
1912
|
-
|
|
1913
|
-
---
|
|
1914
|
-
|
|
1915
|
-
## Task 20: Playground end-to-end smoke + full test suite
|
|
1916
|
-
|
|
1917
|
-
**Files:**
|
|
1918
|
-
- (No code changes — verification only.)
|
|
1919
|
-
|
|
1920
|
-
- [ ] **Step 1: Run the full test suite**
|
|
1921
|
-
|
|
1922
|
-
```bash
|
|
1923
|
-
pnpm test
|
|
1924
|
-
```
|
|
1925
|
-
Expected: All tests green (existing Codex tests + new Claude Code tests).
|
|
1926
|
-
|
|
1927
|
-
- [ ] **Step 2: Run typecheck**
|
|
1928
|
-
|
|
1929
|
-
```bash
|
|
1930
|
-
pnpm typecheck
|
|
1931
|
-
```
|
|
1932
|
-
Expected: 0 errors.
|
|
1933
|
-
|
|
1934
|
-
- [ ] **Step 3: Build**
|
|
1935
|
-
|
|
1936
|
-
```bash
|
|
1937
|
-
pnpm build
|
|
1938
|
-
```
|
|
1939
|
-
Expected: clean build, `dist/` populated.
|
|
1940
|
-
|
|
1941
|
-
- [ ] **Step 4: Playground smoke**
|
|
1942
|
-
|
|
1943
|
-
Start the playground:
|
|
1944
|
-
|
|
1945
|
-
```bash
|
|
1946
|
-
pnpm playground
|
|
1947
|
-
```
|
|
1948
|
-
|
|
1949
|
-
In a browser at http://127.0.0.1:5181, verify:
|
|
1950
|
-
|
|
1951
|
-
1. Filter dropdown shows: "All", "Codex CLI rollout", "AgentWeb codex-team", "Claude Code" — each with non-zero counts where applicable.
|
|
1952
|
-
2. Pick the filter "Claude Code" → list shows only Claude Code entries with the purple "Claude" badge.
|
|
1953
|
-
3. Pick at least 3 different Claude Code files in succession. For each, verify:
|
|
1954
|
-
- The "format: claude-code" appears in the top meta row.
|
|
1955
|
-
- The transcript renders without browser console errors.
|
|
1956
|
-
- For sessions that contained Bash, an exec block renders with `$ <command>`.
|
|
1957
|
-
- For sessions that contained Edit/Write/MultiEdit, a patch block renders with `-` / `+` lines.
|
|
1958
|
-
- For sessions that contained TodoWrite, the plan items render with checkboxes.
|
|
1959
|
-
- No raw "gibberish" text (i.e. thinking signatures are not displayed).
|
|
1960
|
-
4. The "适配后" tab shows event counts > 0 with reasonable type distribution.
|
|
1961
|
-
5. The "原始" tab shows raw JSON with `type`/`uuid`/`sessionId` keys.
|
|
1962
|
-
|
|
1963
|
-
Stop the playground (Ctrl-C).
|
|
1964
|
-
|
|
1965
|
-
- [ ] **Step 5: Verify `subagents/` is excluded**
|
|
1966
|
-
|
|
1967
|
-
Open the file list and confirm no entries under any `subagents/` directory:
|
|
1968
|
-
|
|
1969
|
-
```bash
|
|
1970
|
-
curl -s http://127.0.0.1:5181/api/logs 2>/dev/null | python3 -c "import sys,json; print([f['path'] for f in json.load(sys.stdin)['files'] if '/subagents/' in f['path']])"
|
|
1971
|
-
```
|
|
1972
|
-
|
|
1973
|
-
(Run the curl while playground is up if needed.) Expected: `[]`.
|
|
1974
|
-
|
|
1975
|
-
- [ ] **Step 6: Final commit (none — everything already committed)**
|
|
1976
|
-
|
|
1977
|
-
If anything in the smoke uncovered an issue, fix it in a focused commit. Otherwise, nothing to commit.
|
|
1978
|
-
|
|
1979
|
-
- [ ] **Step 7: Confirm acceptance**
|
|
1980
|
-
|
|
1981
|
-
Walk the acceptance checklist in [the spec, Section 10](../specs/2026-05-15-claude-code-adapter-design.md#10-验收标准) and tick off each item. If any item fails, file a follow-up commit with a fix.
|
|
1982
|
-
|
|
1983
|
-
---
|
|
1984
|
-
|
|
1985
|
-
## Self-Review Notes
|
|
1986
|
-
|
|
1987
|
-
After writing the plan, I walked it against the spec:
|
|
1988
|
-
|
|
1989
|
-
- **§2 Data flow** → Tasks 1, 17, 19 cover detection / scanning / fixtures.
|
|
1990
|
-
- **§3 Top-level type filtering** → Task 3.
|
|
1991
|
-
- **§4 Turn boundary synthesis** (text-user open, multi-turn close, EOF close) → Tasks 4, 5, 6.
|
|
1992
|
-
- **§5.1 Text & thinking mapping** → Tasks 7, 8.
|
|
1993
|
-
- **§5.2 Tool dispatch table** → Tasks 10 (Bash), 11 (TodoWrite), 12 (Edit), 13 (Write), 14 (MultiEdit), 15 (MCP), 16 (fallback).
|
|
1994
|
-
- **§5.3 Diff synthesis** → Tasks 12–14 (helpers added in Task 12).
|
|
1995
|
-
- **§5.4 Pairing strategy via `pending` map** → Established in Task 10, reused in 11–16.
|
|
1996
|
-
- **§6.1 `playground/api.mjs` scan + isAllowed** → Task 17.
|
|
1997
|
-
- **§6.3 `playground/src/App.tsx` chip** → Task 18.
|
|
1998
|
-
- **§7 Error handling** → Tasks 3 (filter), 16 (orphan tool_result fallback).
|
|
1999
|
-
- **§8 Test matrix + anonymized fixtures** → Each adapter task adds tests; Task 19 adds the three fixtures; Task 20 runs full suite.
|
|
2000
|
-
- **§9 Package impact** — `src/` untouched; only playground + fixtures + spec/plan docs.
|
|
2001
|
-
- **§10 Acceptance** → Task 20.
|
|
2002
|
-
|
|
2003
|
-
Token-usage aggregation rule from §4 is implemented in Task 9. Pre-thread `thread_started` once-per-file rule is locked in Task 2. The "no `turn_failed`/`turn_aborted`" decision is implicit — no task ever emits them.
|
|
2004
|
-
|
|
2005
|
-
Plan complete and saved to [docs/superpowers/plans/2026-05-15-claude-code-adapter-implementation.md](2026-05-15-claude-code-adapter-implementation.md).
|