@cleocode/adapters 2026.4.91 → 2026.4.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/index.js +41069 -18278
  2. package/dist/index.js.map +4 -4
  3. package/dist/providers/claude-code/install.d.ts.map +1 -1
  4. package/dist/providers/claude-sdk/index.d.ts +10 -4
  5. package/dist/providers/claude-sdk/index.d.ts.map +1 -1
  6. package/dist/providers/claude-sdk/spawn.d.ts +29 -28
  7. package/dist/providers/claude-sdk/spawn.d.ts.map +1 -1
  8. package/dist/providers/codex/install.d.ts.map +1 -1
  9. package/dist/providers/cursor/install.d.ts.map +1 -1
  10. package/dist/providers/openai-sdk/adapter.d.ts +18 -17
  11. package/dist/providers/openai-sdk/adapter.d.ts.map +1 -1
  12. package/dist/providers/openai-sdk/guardrails.d.ts +71 -18
  13. package/dist/providers/openai-sdk/guardrails.d.ts.map +1 -1
  14. package/dist/providers/openai-sdk/handoff.d.ts +51 -21
  15. package/dist/providers/openai-sdk/handoff.d.ts.map +1 -1
  16. package/dist/providers/openai-sdk/index.d.ts +8 -5
  17. package/dist/providers/openai-sdk/index.d.ts.map +1 -1
  18. package/dist/providers/openai-sdk/install.d.ts +1 -1
  19. package/dist/providers/openai-sdk/spawn.d.ts +54 -21
  20. package/dist/providers/openai-sdk/spawn.d.ts.map +1 -1
  21. package/dist/providers/openai-sdk/tracing.d.ts +87 -21
  22. package/dist/providers/openai-sdk/tracing.d.ts.map +1 -1
  23. package/dist/providers/opencode/install.d.ts.map +1 -1
  24. package/dist/providers/opencode/spawn.d.ts.map +1 -1
  25. package/dist/providers/pi/install.d.ts.map +1 -1
  26. package/dist/providers/shared/paths.d.ts +32 -0
  27. package/dist/providers/shared/paths.d.ts.map +1 -0
  28. package/dist/providers/shared/sdk-result-mapper.d.ts +9 -7
  29. package/dist/providers/shared/sdk-result-mapper.d.ts.map +1 -1
  30. package/package.json +6 -5
  31. package/src/__tests__/claude-code-adapter.test.ts +9 -4
  32. package/src/__tests__/cursor-adapter.test.ts +9 -8
  33. package/src/__tests__/harness-interop.test.ts +451 -0
  34. package/src/__tests__/opencode-adapter.test.ts +9 -4
  35. package/src/providers/claude-code/install.ts +10 -2
  36. package/src/providers/claude-sdk/__tests__/spawn.test.ts +100 -265
  37. package/src/providers/claude-sdk/index.ts +10 -4
  38. package/src/providers/claude-sdk/spawn.ts +69 -106
  39. package/src/providers/codex/install.ts +10 -2
  40. package/src/providers/cursor/install.ts +10 -2
  41. package/src/providers/openai-sdk/__tests__/openai-sdk-spawn.test.ts +134 -103
  42. package/src/providers/openai-sdk/adapter.ts +19 -18
  43. package/src/providers/openai-sdk/guardrails.ts +106 -25
  44. package/src/providers/openai-sdk/handoff.ts +73 -37
  45. package/src/providers/openai-sdk/index.ts +28 -4
  46. package/src/providers/openai-sdk/install.ts +1 -1
  47. package/src/providers/openai-sdk/manifest.json +4 -4
  48. package/src/providers/openai-sdk/spawn.ts +213 -48
  49. package/src/providers/openai-sdk/tracing.ts +105 -22
  50. package/src/providers/opencode/install.ts +10 -2
  51. package/src/providers/opencode/spawn.ts +2 -1
  52. package/src/providers/pi/install.ts +10 -2
  53. package/src/providers/shared/paths.ts +79 -0
  54. package/src/providers/shared/sdk-result-mapper.ts +9 -7
@@ -1,32 +1,50 @@
1
1
  /**
2
- * Tests for ClaudeSDKSpawnProvider
2
+ * Tests for ClaudeSDKSpawnProvider — Vercel AI SDK edition.
3
3
  *
4
- * The SDK `query()` function is mocked so tests run without a real
4
+ * The Vercel AI SDK `generateText` call is mocked via a CLEO-native mock
5
+ * that returns a deterministic response. Tests run without a real
5
6
  * ANTHROPIC_API_KEY or network connection.
6
7
  *
7
- * @task T581
8
+ * @task T581 (original)
9
+ * @task T933 (SDK consolidation — Vercel AI SDK migration)
8
10
  */
9
11
 
10
12
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
13
  import { ClaudeSDKSpawnProvider } from '../spawn.js';
12
14
 
13
15
  // ---------------------------------------------------------------------------
14
- // Mock the SDK module
16
+ // Mock Vercel AI SDK surface
15
17
  // ---------------------------------------------------------------------------
16
18
 
17
- /** Minimal SDK message iterator builder for tests. */
18
- function makeQueryIterator(messages: Array<Record<string, unknown>>) {
19
+ /** Shared mock state tracked across tests. */
20
+ const { mockState } = vi.hoisted(() => {
19
21
  return {
20
- [Symbol.asyncIterator]: async function* () {
21
- for (const msg of messages) {
22
- yield msg;
23
- }
22
+ mockState: {
23
+ text: 'mocked claude response',
24
+ shouldThrow: false,
25
+ lastCall: null as null | { model: unknown; prompt: string },
24
26
  },
25
27
  };
26
- }
28
+ });
29
+
30
+ // Mock the '@ai-sdk/anthropic' surface: createAnthropic returns a factory
31
+ // function that produces a LanguageModel stand-in. CLEO only consumes the
32
+ // return value as an opaque handle passed to generateText.
33
+ vi.mock('@ai-sdk/anthropic', () => ({
34
+ createAnthropic: vi.fn((_config: { apiKey: string }) => {
35
+ return (modelId: string) => ({ __cleoMockModel: true, modelId });
36
+ }),
37
+ }));
27
38
 
28
- vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
29
- query: vi.fn(),
39
+ // Mock 'ai' generateText to drive deterministic test outputs.
40
+ vi.mock('ai', () => ({
41
+ generateText: vi.fn(async ({ model, prompt }: { model: unknown; prompt: string }) => {
42
+ mockState.lastCall = { model, prompt };
43
+ if (mockState.shouldThrow) {
44
+ throw new Error('mock AI SDK error');
45
+ }
46
+ return { text: mockState.text };
47
+ }),
30
48
  }));
31
49
 
32
50
  // Mock CANT enrichment so tests don't need the cleo CLI.
@@ -36,15 +54,6 @@ vi.mock('../../../cant-context.js', () => ({
36
54
  ),
37
55
  }));
38
56
 
39
- // ---------------------------------------------------------------------------
40
- // Helpers
41
- // ---------------------------------------------------------------------------
42
-
43
- async function getQueryMock() {
44
- const { query } = await import('@anthropic-ai/claude-agent-sdk');
45
- return query as ReturnType<typeof vi.fn>;
46
- }
47
-
48
57
  // ---------------------------------------------------------------------------
49
58
  // Tests
50
59
  // ---------------------------------------------------------------------------
@@ -54,10 +63,14 @@ describe('ClaudeSDKSpawnProvider', () => {
54
63
 
55
64
  beforeEach(() => {
56
65
  provider = new ClaudeSDKSpawnProvider();
66
+ mockState.shouldThrow = false;
67
+ mockState.text = 'mocked claude response';
68
+ mockState.lastCall = null;
57
69
  });
58
70
 
59
71
  afterEach(() => {
60
72
  vi.restoreAllMocks();
73
+ mockState.shouldThrow = false;
61
74
  });
62
75
 
63
76
  // -------------------------------------------------------------------------
@@ -71,13 +84,9 @@ describe('ClaudeSDKSpawnProvider', () => {
71
84
  });
72
85
 
73
86
  it('returns false when no credentials are available', async () => {
74
- // Mock fs.existsSync to return false for all key paths so no tier
75
- // resolves (env var, stored key file, or OAuth credentials file).
76
- const { existsSync } = await import('node:fs');
77
87
  const saved = process.env.ANTHROPIC_API_KEY;
78
88
  delete process.env.ANTHROPIC_API_KEY;
79
- vi.spyOn({ existsSync }, 'existsSync').mockReturnValue(false);
80
- // Use vi.mock for node:fs to prevent reading real ~/.claude/.credentials.json
89
+ // Mock node:fs so the stored-key file and OAuth creds both fail to resolve.
81
90
  vi.doMock('node:fs', () => ({
82
91
  existsSync: vi.fn().mockReturnValue(false),
83
92
  readFileSync: vi.fn().mockImplementation(() => {
@@ -85,7 +94,6 @@ describe('ClaudeSDKSpawnProvider', () => {
85
94
  }),
86
95
  }));
87
96
  try {
88
- // Re-import spawn module with mocked fs to get fresh resolver state
89
97
  vi.resetModules();
90
98
  const { ClaudeSDKSpawnProvider: FreshProvider } = await import('../spawn.js');
91
99
  const freshProvider = new FreshProvider();
@@ -153,48 +161,15 @@ describe('ClaudeSDKSpawnProvider', () => {
153
161
  // -------------------------------------------------------------------------
154
162
 
155
163
  describe('spawn() — success', () => {
156
- it('returns completed status with aggregated output', async () => {
157
- const queryMock = await getQueryMock();
158
- queryMock.mockReturnValue(
159
- makeQueryIterator([
160
- {
161
- type: 'system',
162
- subtype: 'init',
163
- session_id: 'sess-abc',
164
- tools: [],
165
- mcp_servers: [],
166
- model: 'claude-sonnet-4-5',
167
- permissionMode: 'bypassPermissions',
168
- cwd: '/tmp',
169
- slash_commands: [],
170
- output_style: 'auto',
171
- },
172
- {
173
- type: 'assistant',
174
- session_id: 'sess-abc',
175
- message: {
176
- content: [
177
- { type: 'text', text: 'Hello from' },
178
- { type: 'text', text: ' the SDK.' },
179
- ],
180
- },
181
- },
182
- {
183
- type: 'result',
184
- subtype: 'success',
185
- session_id: 'sess-abc',
186
- result: 'Done.',
187
- is_error: false,
188
- duration_ms: 100,
189
- duration_api_ms: 80,
190
- num_turns: 1,
191
- total_cost_usd: 0.001,
192
- usage: {},
193
- modelUsage: {},
194
- permission_denials: [],
195
- },
196
- ]),
197
- );
164
+ beforeEach(() => {
165
+ process.env.ANTHROPIC_API_KEY = 'test-key';
166
+ });
167
+ afterEach(() => {
168
+ delete process.env.ANTHROPIC_API_KEY;
169
+ });
170
+
171
+ it('returns completed status with generated output', async () => {
172
+ mockState.text = 'Hello from the SDK.';
198
173
 
199
174
  const result = await provider.spawn({
200
175
  taskId: 'T001',
@@ -205,113 +180,44 @@ describe('ClaudeSDKSpawnProvider', () => {
205
180
  expect(result.status).toBe('completed');
206
181
  expect(result.providerId).toBe('claude-sdk');
207
182
  expect(result.taskId).toBe('T001');
208
- expect(result.output).toContain('Hello from');
209
- expect(result.output).toContain('the SDK.');
210
- expect(result.output).toContain('Done.');
183
+ expect(result.output).toBe('Hello from the SDK.');
211
184
  expect(result.exitCode).toBe(0);
212
185
  expect(result.startTime).toBeTruthy();
213
186
  expect(result.endTime).toBeTruthy();
214
187
  });
215
188
 
216
- it('passes allowedTools from context options', async () => {
217
- const queryMock = await getQueryMock();
218
- queryMock.mockReturnValue(
219
- makeQueryIterator([
220
- {
221
- type: 'result',
222
- subtype: 'success',
223
- session_id: 'sess-xyz',
224
- result: '',
225
- is_error: false,
226
- duration_ms: 10,
227
- duration_api_ms: 8,
228
- num_turns: 1,
229
- total_cost_usd: 0,
230
- usage: {},
231
- modelUsage: {},
232
- permission_denials: [],
233
- },
234
- ]),
235
- );
236
-
189
+ it('forwards the enriched prompt to generateText', async () => {
237
190
  await provider.spawn({
238
191
  taskId: 'T002',
239
- prompt: 'Read only.',
240
- options: { toolAllowlist: ['Read', 'Grep'] },
192
+ prompt: 'Describe the project.',
241
193
  });
242
194
 
243
- expect(queryMock).toHaveBeenCalledWith(
244
- expect.objectContaining({
245
- options: expect.objectContaining({
246
- allowedTools: ['Read', 'Grep'],
247
- }),
248
- }),
249
- );
195
+ expect(mockState.lastCall?.prompt).toBe('Describe the project.');
250
196
  });
251
197
 
252
- it('uses default tools when no toolAllowlist provided', async () => {
253
- const queryMock = await getQueryMock();
254
- queryMock.mockReturnValue(
255
- makeQueryIterator([
256
- {
257
- type: 'result',
258
- subtype: 'success',
259
- session_id: 'sess-xyz',
260
- result: '',
261
- is_error: false,
262
- duration_ms: 10,
263
- duration_api_ms: 8,
264
- num_turns: 1,
265
- total_cost_usd: 0,
266
- usage: {},
267
- modelUsage: {},
268
- permission_denials: [],
269
- },
270
- ]),
271
- );
272
-
273
- await provider.spawn({ taskId: 'T003', prompt: 'Default tools.' });
274
-
275
- expect(queryMock).toHaveBeenCalledWith(
276
- expect.objectContaining({
277
- options: expect.objectContaining({
278
- allowedTools: expect.arrayContaining(['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep']),
279
- }),
280
- }),
281
- );
198
+ it('uses the default model when none is specified', async () => {
199
+ await provider.spawn({
200
+ taskId: 'T003',
201
+ prompt: 'Default model.',
202
+ });
203
+
204
+ const model = mockState.lastCall?.model as
205
+ | { __cleoMockModel: true; modelId: string }
206
+ | undefined;
207
+ expect(model?.modelId).toBe('claude-sonnet-4-5');
282
208
  });
283
209
 
284
- it('sets permissionMode to bypassPermissions', async () => {
285
- const queryMock = await getQueryMock();
286
- queryMock.mockReturnValue(
287
- makeQueryIterator([
288
- {
289
- type: 'result',
290
- subtype: 'success',
291
- session_id: 'sess-perm',
292
- result: '',
293
- is_error: false,
294
- duration_ms: 5,
295
- duration_api_ms: 4,
296
- num_turns: 1,
297
- total_cost_usd: 0,
298
- usage: {},
299
- modelUsage: {},
300
- permission_denials: [],
301
- },
302
- ]),
303
- );
304
-
305
- await provider.spawn({ taskId: 'T004', prompt: 'Permissions.' });
306
-
307
- expect(queryMock).toHaveBeenCalledWith(
308
- expect.objectContaining({
309
- options: expect.objectContaining({
310
- permissionMode: 'bypassPermissions',
311
- allowDangerouslySkipPermissions: true,
312
- }),
313
- }),
314
- );
210
+ it('uses the requested model when provided in options', async () => {
211
+ await provider.spawn({
212
+ taskId: 'T004',
213
+ prompt: 'Override model.',
214
+ options: { model: 'claude-sonnet-4-6' },
215
+ });
216
+
217
+ const model = mockState.lastCall?.model as
218
+ | { __cleoMockModel: true; modelId: string }
219
+ | undefined;
220
+ expect(model?.modelId).toBe('claude-sonnet-4-6');
315
221
  });
316
222
  });
317
223
 
@@ -320,114 +226,43 @@ describe('ClaudeSDKSpawnProvider', () => {
320
226
  // -------------------------------------------------------------------------
321
227
 
322
228
  describe('spawn() — error handling', () => {
323
- it('returns failed status on SDK error subtype', async () => {
324
- const queryMock = await getQueryMock();
325
- queryMock.mockReturnValue(
326
- makeQueryIterator([
327
- {
328
- type: 'result',
329
- subtype: 'error_during_execution',
330
- session_id: 'sess-err',
331
- is_error: true,
332
- errors: ['Something went wrong'],
333
- duration_ms: 50,
334
- duration_api_ms: 40,
335
- num_turns: 1,
336
- total_cost_usd: 0,
337
- usage: {},
338
- modelUsage: {},
339
- permission_denials: [],
340
- },
341
- ]),
342
- );
343
-
344
- const result = await provider.spawn({ taskId: 'T005', prompt: 'Fail.' });
345
-
346
- expect(result.status).toBe('failed');
347
- expect(result.exitCode).toBe(1);
348
- expect(result.error).toContain('Something went wrong');
229
+ it('returns failed status when no Anthropic credentials exist', async () => {
230
+ const saved = process.env.ANTHROPIC_API_KEY;
231
+ delete process.env.ANTHROPIC_API_KEY;
232
+ vi.doMock('node:fs', () => ({
233
+ existsSync: vi.fn().mockReturnValue(false),
234
+ readFileSync: vi.fn().mockImplementation(() => {
235
+ throw new Error('mocked: file not found');
236
+ }),
237
+ }));
238
+ try {
239
+ vi.resetModules();
240
+ const { ClaudeSDKSpawnProvider: FreshProvider } = await import('../spawn.js');
241
+ const freshProvider = new FreshProvider();
242
+ const result = await freshProvider.spawn({
243
+ taskId: 'T005',
244
+ prompt: 'No creds.',
245
+ });
246
+ expect(result.status).toBe('failed');
247
+ expect(result.exitCode).toBe(1);
248
+ expect(result.error).toContain('No Anthropic credentials');
249
+ } finally {
250
+ vi.resetModules();
251
+ vi.doUnmock('node:fs');
252
+ if (saved !== undefined) process.env.ANTHROPIC_API_KEY = saved;
253
+ }
349
254
  });
350
255
 
351
- it('returns failed status when query() throws', async () => {
352
- const queryMock = await getQueryMock();
353
- queryMock.mockImplementation(() => {
354
- throw new Error('Network error');
355
- });
256
+ it('returns failed status when generateText throws', async () => {
257
+ process.env.ANTHROPIC_API_KEY = 'test-key';
258
+ mockState.shouldThrow = true;
356
259
 
357
260
  const result = await provider.spawn({ taskId: 'T006', prompt: 'Throw.' });
358
261
 
359
262
  expect(result.status).toBe('failed');
360
263
  expect(result.exitCode).toBe(1);
361
- expect(result.error).toContain('Network error');
362
- });
363
-
364
- it('returns failed status when error_max_turns reached', async () => {
365
- const queryMock = await getQueryMock();
366
- queryMock.mockReturnValue(
367
- makeQueryIterator([
368
- {
369
- type: 'result',
370
- subtype: 'error_max_turns',
371
- session_id: 'sess-max',
372
- is_error: true,
373
- errors: [],
374
- duration_ms: 200,
375
- duration_api_ms: 180,
376
- num_turns: 10,
377
- total_cost_usd: 0.05,
378
- usage: {},
379
- modelUsage: {},
380
- permission_denials: [],
381
- },
382
- ]),
383
- );
384
-
385
- const result = await provider.spawn({ taskId: 'T007', prompt: 'Max turns.' });
386
-
387
- expect(result.status).toBe('failed');
388
- expect(result.exitCode).toBe(1);
389
- });
390
- });
391
-
392
- // -------------------------------------------------------------------------
393
- // Session resume
394
- // -------------------------------------------------------------------------
395
-
396
- describe('spawn() — session resume', () => {
397
- it('passes resume option when resumeSessionId is provided', async () => {
398
- const queryMock = await getQueryMock();
399
- queryMock.mockReturnValue(
400
- makeQueryIterator([
401
- {
402
- type: 'result',
403
- subtype: 'success',
404
- session_id: 'sess-resume',
405
- result: 'resumed',
406
- is_error: false,
407
- duration_ms: 20,
408
- duration_api_ms: 15,
409
- num_turns: 1,
410
- total_cost_usd: 0,
411
- usage: {},
412
- modelUsage: {},
413
- permission_denials: [],
414
- },
415
- ]),
416
- );
417
-
418
- await provider.spawn({
419
- taskId: 'T008',
420
- prompt: 'Continue work.',
421
- options: { resumeSessionId: 'prior-session-id' },
422
- });
423
-
424
- expect(queryMock).toHaveBeenCalledWith(
425
- expect.objectContaining({
426
- options: expect.objectContaining({
427
- resume: 'prior-session-id',
428
- }),
429
- }),
430
- );
264
+ expect(result.error).toContain('mock AI SDK error');
265
+ delete process.env.ANTHROPIC_API_KEY;
431
266
  });
432
267
  });
433
268
  });
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * @packageDocumentation
3
3
  *
4
- * Claude Agent SDK spawn provider for CLEO.
5
- * Uses `@anthropic-ai/claude-agent-sdk` instead of the CLI for programmatic
6
- * subagent execution with structured output and session tracking.
4
+ * Claude SDK spawn provider for CLEO — Vercel AI SDK edition.
5
+ *
6
+ * Uses `@ai-sdk/anthropic` via the Vercel AI SDK (`ai` v6) instead of the
7
+ * legacy `@anthropic-ai/claude-agent-sdk`. CLEO retains its own orchestration
8
+ * primitives (composeSpawnPayload, playbook runtime, agent registry); this
9
+ * provider exposes the LLM bridge for programmatic subagent execution with
10
+ * structured output and session tracking.
7
11
  *
8
12
  * Enabled via `provider.claude.mode = 'sdk'` in CLEO config.
9
13
  *
10
- * @task T581
14
+ * @task T581 (original)
15
+ * @task T933 (SDK consolidation — Vercel AI SDK migration)
16
+ * @see ADR-052 — SDK consolidation decision
11
17
  */
12
18
 
13
19
  export type { McpServerMap, McpStdioConfig } from './mcp-registry.js';