@boostecom/provider 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +90 -0
  2. package/dist/index.cjs +2522 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +848 -0
  5. package/dist/index.d.ts +848 -0
  6. package/dist/index.js +2484 -0
  7. package/dist/index.js.map +1 -0
  8. package/docs/content/README.md +337 -0
  9. package/docs/content/agent-teams.mdx +324 -0
  10. package/docs/content/api.mdx +757 -0
  11. package/docs/content/best-practices.mdx +624 -0
  12. package/docs/content/examples.mdx +675 -0
  13. package/docs/content/guide.mdx +516 -0
  14. package/docs/content/index.mdx +99 -0
  15. package/docs/content/installation.mdx +246 -0
  16. package/docs/content/skills.mdx +548 -0
  17. package/docs/content/troubleshooting.mdx +588 -0
  18. package/docs/examples/README.md +499 -0
  19. package/docs/examples/abort-signal.ts +125 -0
  20. package/docs/examples/agent-teams.ts +122 -0
  21. package/docs/examples/basic-usage.ts +73 -0
  22. package/docs/examples/check-cli.ts +51 -0
  23. package/docs/examples/conversation-history.ts +69 -0
  24. package/docs/examples/custom-config.ts +90 -0
  25. package/docs/examples/generate-object-constraints.ts +209 -0
  26. package/docs/examples/generate-object.ts +211 -0
  27. package/docs/examples/hooks-callbacks.ts +63 -0
  28. package/docs/examples/images.ts +76 -0
  29. package/docs/examples/integration-test.ts +241 -0
  30. package/docs/examples/limitations.ts +150 -0
  31. package/docs/examples/logging-custom-logger.ts +99 -0
  32. package/docs/examples/logging-default.ts +55 -0
  33. package/docs/examples/logging-disabled.ts +74 -0
  34. package/docs/examples/logging-verbose.ts +64 -0
  35. package/docs/examples/long-running-tasks.ts +179 -0
  36. package/docs/examples/message-injection.ts +210 -0
  37. package/docs/examples/mid-stream-injection.ts +126 -0
  38. package/docs/examples/run-all-examples.sh +48 -0
  39. package/docs/examples/sdk-tools-callbacks.ts +49 -0
  40. package/docs/examples/skills-discovery.ts +144 -0
  41. package/docs/examples/skills-management.ts +140 -0
  42. package/docs/examples/stream-object.ts +80 -0
  43. package/docs/examples/streaming.ts +52 -0
  44. package/docs/examples/structured-output-repro.ts +227 -0
  45. package/docs/examples/tool-management.ts +215 -0
  46. package/docs/examples/tool-streaming.ts +132 -0
  47. package/docs/examples/zod4-compatibility-test.ts +290 -0
  48. package/docs/src/claude-code-language-model.test.ts +3883 -0
  49. package/docs/src/claude-code-language-model.ts +2586 -0
  50. package/docs/src/claude-code-provider.test.ts +97 -0
  51. package/docs/src/claude-code-provider.ts +179 -0
  52. package/docs/src/convert-to-claude-code-messages.images.test.ts +104 -0
  53. package/docs/src/convert-to-claude-code-messages.test.ts +193 -0
  54. package/docs/src/convert-to-claude-code-messages.ts +419 -0
  55. package/docs/src/errors.test.ts +213 -0
  56. package/docs/src/errors.ts +216 -0
  57. package/docs/src/index.test.ts +49 -0
  58. package/docs/src/index.ts +98 -0
  59. package/docs/src/logger.integration.test.ts +164 -0
  60. package/docs/src/logger.test.ts +184 -0
  61. package/docs/src/logger.ts +65 -0
  62. package/docs/src/map-claude-code-finish-reason.test.ts +120 -0
  63. package/docs/src/map-claude-code-finish-reason.ts +60 -0
  64. package/docs/src/mcp-helpers.test.ts +71 -0
  65. package/docs/src/mcp-helpers.ts +123 -0
  66. package/docs/src/message-injection.test.ts +460 -0
  67. package/docs/src/types.ts +447 -0
  68. package/docs/src/validation.test.ts +558 -0
  69. package/docs/src/validation.ts +360 -0
  70. package/package.json +124 -0
@@ -0,0 +1,460 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ClaudeCodeLanguageModel } from './claude-code-language-model.js';
3
+ import { claudeCodeSettingsSchema } from './validation.js';
4
+ import type { MessageInjector } from './types.js';
5
+
6
+ // Mock the SDK module
7
+ vi.mock('@anthropic-ai/claude-agent-sdk', () => {
8
+ return {
9
+ query: vi.fn(),
10
+ };
11
+ });
12
+
13
+ import { query as mockQuery } from '@anthropic-ai/claude-agent-sdk';
14
+ import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
15
+
16
+ describe('MessageInjector', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ describe('validation', () => {
22
+ it('should accept onStreamStart as a function', () => {
23
+ const settings = {
24
+ onStreamStart: () => {},
25
+ };
26
+ const result = claudeCodeSettingsSchema.safeParse(settings);
27
+ expect(result.success).toBe(true);
28
+ });
29
+
30
+ it('should reject onStreamStart if not a function', () => {
31
+ const settings = {
32
+ onStreamStart: 'not-a-function',
33
+ };
34
+ const result = claudeCodeSettingsSchema.safeParse(settings);
35
+ expect(result.success).toBe(false);
36
+ });
37
+
38
+ it('should accept settings without onStreamStart', () => {
39
+ const settings = {};
40
+ const result = claudeCodeSettingsSchema.safeParse(settings);
41
+ expect(result.success).toBe(true);
42
+ });
43
+ });
44
+
45
+ describe('onStreamStart callback', () => {
46
+ it('should call onStreamStart when streaming input is enabled and prompt is iterated', async () => {
47
+ const onStreamStart = vi.fn((injector) => {
48
+ // Close immediately so the stream can complete
49
+ injector.close();
50
+ });
51
+ const model = new ClaudeCodeLanguageModel({
52
+ id: 'sonnet',
53
+ settings: {
54
+ streamingInput: 'always',
55
+ onStreamStart,
56
+ },
57
+ });
58
+
59
+ // Mock that iterates the prompt (which triggers onStreamStart)
60
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
61
+ // Consume the async iterable to trigger onStreamStart
62
+ void (async () => {
63
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
64
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
65
+ for await (const _ of prompt) {
66
+ // Just iterate to trigger the callback
67
+ }
68
+ }
69
+ })();
70
+
71
+ return {
72
+ async *[Symbol.asyncIterator]() {
73
+ await new Promise((r) => setTimeout(r, 50));
74
+ yield {
75
+ type: 'result',
76
+ subtype: 'success',
77
+ session_id: 'test-session',
78
+ usage: { input_tokens: 10, output_tokens: 20 },
79
+ };
80
+ },
81
+ } as any;
82
+ });
83
+
84
+ await model.doGenerate({
85
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
86
+ } as any);
87
+
88
+ expect(onStreamStart).toHaveBeenCalledTimes(1);
89
+ expect(onStreamStart).toHaveBeenCalledWith(
90
+ expect.objectContaining({
91
+ inject: expect.any(Function),
92
+ close: expect.any(Function),
93
+ })
94
+ );
95
+ });
96
+
97
+ it('should not call onStreamStart when streamingInput is off', async () => {
98
+ const onStreamStart = vi.fn();
99
+ const model = new ClaudeCodeLanguageModel({
100
+ id: 'sonnet',
101
+ settings: {
102
+ streamingInput: 'off',
103
+ onStreamStart,
104
+ },
105
+ });
106
+
107
+ vi.mocked(mockQuery).mockImplementation(() => {
108
+ // With streamingInput off, prompt is a string, not AsyncIterable
109
+ // So onStreamStart should never be called
110
+ return {
111
+ async *[Symbol.asyncIterator]() {
112
+ yield {
113
+ type: 'result',
114
+ subtype: 'success',
115
+ session_id: 'test-session',
116
+ usage: { input_tokens: 10, output_tokens: 20 },
117
+ };
118
+ },
119
+ } as any;
120
+ });
121
+
122
+ await model.doGenerate({
123
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
124
+ } as any);
125
+
126
+ expect(onStreamStart).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it('should provide a working MessageInjector when prompt is iterated', async () => {
130
+ let capturedInjector: MessageInjector | null = null;
131
+ const model = new ClaudeCodeLanguageModel({
132
+ id: 'sonnet',
133
+ settings: {
134
+ streamingInput: 'always',
135
+ onStreamStart: (injector) => {
136
+ capturedInjector = injector;
137
+ injector.close();
138
+ },
139
+ },
140
+ });
141
+
142
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
143
+ void (async () => {
144
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
145
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
146
+ for await (const _ of prompt) {
147
+ // Iterate to trigger callback
148
+ }
149
+ }
150
+ })();
151
+
152
+ return {
153
+ async *[Symbol.asyncIterator]() {
154
+ await new Promise((r) => setTimeout(r, 50));
155
+ yield {
156
+ type: 'result',
157
+ subtype: 'success',
158
+ session_id: 'test-session',
159
+ usage: { input_tokens: 10, output_tokens: 20 },
160
+ };
161
+ },
162
+ } as any;
163
+ });
164
+
165
+ await model.doGenerate({
166
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
167
+ } as any);
168
+
169
+ expect(capturedInjector).not.toBeNull();
170
+ expect(typeof capturedInjector!.inject).toBe('function');
171
+ expect(typeof capturedInjector!.close).toBe('function');
172
+ });
173
+ });
174
+
175
+ describe('message injection flow', () => {
176
+ it('should pass injected messages to the SDK prompt', async () => {
177
+ const collectedMessages: SDKUserMessage[] = [];
178
+
179
+ const model = new ClaudeCodeLanguageModel({
180
+ id: 'sonnet',
181
+ settings: {
182
+ streamingInput: 'always',
183
+ onStreamStart: (injector) => {
184
+ // Inject a message immediately
185
+ injector.inject('Injected message!');
186
+ // Then close to let the stream complete
187
+ injector.close();
188
+ },
189
+ },
190
+ });
191
+
192
+ // Create a mock that collects all yielded messages from the prompt
193
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
194
+ // Consume the async iterable in the background
195
+ void (async () => {
196
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
197
+ for await (const msg of prompt) {
198
+ collectedMessages.push(msg);
199
+ }
200
+ }
201
+ })();
202
+
203
+ // Return a mock response
204
+ return {
205
+ async *[Symbol.asyncIterator]() {
206
+ // Small delay to let injection happen
207
+ await new Promise((r) => setTimeout(r, 50));
208
+ yield {
209
+ type: 'result',
210
+ subtype: 'success',
211
+ session_id: 'test-session',
212
+ usage: { input_tokens: 10, output_tokens: 20 },
213
+ };
214
+ },
215
+ } as any;
216
+ });
217
+
218
+ await model.doGenerate({
219
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
220
+ } as any);
221
+
222
+ // Should have initial message + injected message
223
+ expect(collectedMessages.length).toBeGreaterThanOrEqual(1);
224
+
225
+ // First message should be the initial user message
226
+ expect(collectedMessages[0].type).toBe('user');
227
+ expect(collectedMessages[0].message.role).toBe('user');
228
+ });
229
+
230
+ it('should handle multiple injected messages', async () => {
231
+ const collectedMessages: SDKUserMessage[] = [];
232
+
233
+ const model = new ClaudeCodeLanguageModel({
234
+ id: 'sonnet',
235
+ settings: {
236
+ streamingInput: 'always',
237
+ onStreamStart: (injector) => {
238
+ // Inject multiple messages
239
+ injector.inject('First injection');
240
+ injector.inject('Second injection');
241
+ injector.inject('Third injection');
242
+ injector.close();
243
+ },
244
+ },
245
+ });
246
+
247
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
248
+ void (async () => {
249
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
250
+ for await (const msg of prompt) {
251
+ collectedMessages.push(msg);
252
+ }
253
+ }
254
+ })();
255
+
256
+ return {
257
+ async *[Symbol.asyncIterator]() {
258
+ await new Promise((r) => setTimeout(r, 100));
259
+ yield {
260
+ type: 'result',
261
+ subtype: 'success',
262
+ session_id: 'test-session',
263
+ usage: { input_tokens: 10, output_tokens: 20 },
264
+ };
265
+ },
266
+ } as any;
267
+ });
268
+
269
+ await model.doGenerate({
270
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
271
+ } as any);
272
+
273
+ // Should have initial + 3 injected messages = 4 total
274
+ // (though timing may affect this in tests)
275
+ expect(collectedMessages.length).toBeGreaterThanOrEqual(1);
276
+ });
277
+
278
+ it('should ignore inject calls after close', async () => {
279
+ const model = new ClaudeCodeLanguageModel({
280
+ id: 'sonnet',
281
+ settings: {
282
+ streamingInput: 'always',
283
+ onStreamStart: (injector) => {
284
+ injector.close();
285
+ // This should be ignored
286
+ injector.inject('Should be ignored');
287
+ },
288
+ },
289
+ });
290
+
291
+ const mockResponse = {
292
+ async *[Symbol.asyncIterator]() {
293
+ yield {
294
+ type: 'result',
295
+ subtype: 'success',
296
+ session_id: 'test-session',
297
+ usage: { input_tokens: 10, output_tokens: 20 },
298
+ };
299
+ },
300
+ };
301
+ vi.mocked(mockQuery).mockReturnValue(mockResponse as any);
302
+
303
+ // Should not throw
304
+ await expect(
305
+ model.doGenerate({
306
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
307
+ } as any)
308
+ ).resolves.toBeDefined();
309
+ });
310
+ });
311
+
312
+ describe('delivery callback', () => {
313
+ it('should call onResult with true when message is delivered', async () => {
314
+ const deliveryResults: boolean[] = [];
315
+
316
+ const model = new ClaudeCodeLanguageModel({
317
+ id: 'sonnet',
318
+ settings: {
319
+ streamingInput: 'always',
320
+ onStreamStart: (injector) => {
321
+ injector.inject('Test message', (delivered) => {
322
+ deliveryResults.push(delivered);
323
+ });
324
+ injector.close();
325
+ },
326
+ },
327
+ });
328
+
329
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
330
+ void (async () => {
331
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
332
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
333
+ for await (const _ of prompt) {
334
+ // Iterate to trigger delivery
335
+ }
336
+ }
337
+ })();
338
+
339
+ return {
340
+ async *[Symbol.asyncIterator]() {
341
+ await new Promise((r) => setTimeout(r, 100));
342
+ yield {
343
+ type: 'result',
344
+ subtype: 'success',
345
+ session_id: 'test-session',
346
+ usage: { input_tokens: 10, output_tokens: 20 },
347
+ };
348
+ },
349
+ } as any;
350
+ });
351
+
352
+ await model.doGenerate({
353
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
354
+ } as any);
355
+
356
+ expect(deliveryResults).toContain(true);
357
+ });
358
+
359
+ it('should call onResult with false when session ends before delivery', async () => {
360
+ const deliveryResults: boolean[] = [];
361
+
362
+ const model = new ClaudeCodeLanguageModel({
363
+ id: 'sonnet',
364
+ settings: {
365
+ streamingInput: 'always',
366
+ onStreamStart: (injector) => {
367
+ // Inject after a delay - session will end first
368
+ setTimeout(() => {
369
+ injector.inject('Too late message', (delivered) => {
370
+ deliveryResults.push(delivered);
371
+ });
372
+ }, 50);
373
+ },
374
+ },
375
+ });
376
+
377
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
378
+ // Start iterating prompt but end session quickly
379
+ void (async () => {
380
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
381
+ const iter = prompt[Symbol.asyncIterator]();
382
+ await iter.next(); // Get initial message
383
+ // Call next() again to resume generator and trigger onStreamStart
384
+ // Don't await - let it run in background while session ends
385
+ iter.next();
386
+ }
387
+ })();
388
+
389
+ return {
390
+ async *[Symbol.asyncIterator]() {
391
+ // Small delay then end - session ends before the 50ms inject
392
+ await new Promise((r) => setTimeout(r, 20));
393
+ yield {
394
+ type: 'result',
395
+ subtype: 'success',
396
+ session_id: 'test-session',
397
+ usage: { input_tokens: 10, output_tokens: 20 },
398
+ };
399
+ },
400
+ } as any;
401
+ });
402
+
403
+ await model.doGenerate({
404
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
405
+ } as any);
406
+
407
+ // Wait for the delayed inject to happen and be processed
408
+ await new Promise((r) => setTimeout(r, 150));
409
+
410
+ expect(deliveryResults).toContain(false);
411
+ });
412
+
413
+ it('should call onResult with false when inject is called after close', async () => {
414
+ const deliveryResults: boolean[] = [];
415
+
416
+ const model = new ClaudeCodeLanguageModel({
417
+ id: 'sonnet',
418
+ settings: {
419
+ streamingInput: 'always',
420
+ onStreamStart: (injector) => {
421
+ injector.close();
422
+ injector.inject('After close', (delivered) => {
423
+ deliveryResults.push(delivered);
424
+ });
425
+ },
426
+ },
427
+ });
428
+
429
+ vi.mocked(mockQuery).mockImplementation(({ prompt }: any) => {
430
+ // Iterate prompt to trigger onStreamStart
431
+ void (async () => {
432
+ if (prompt && typeof prompt[Symbol.asyncIterator] === 'function') {
433
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
434
+ for await (const _ of prompt) {
435
+ // Iterate to trigger callback
436
+ }
437
+ }
438
+ })();
439
+
440
+ return {
441
+ async *[Symbol.asyncIterator]() {
442
+ await new Promise((r) => setTimeout(r, 50));
443
+ yield {
444
+ type: 'result',
445
+ subtype: 'success',
446
+ session_id: 'test-session',
447
+ usage: { input_tokens: 10, output_tokens: 20 },
448
+ };
449
+ },
450
+ } as any;
451
+ });
452
+
453
+ await model.doGenerate({
454
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
455
+ } as any);
456
+
457
+ expect(deliveryResults).toEqual([false]);
458
+ });
459
+ });
460
+ });