@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,419 @@
1
+ import type { ModelMessage } from 'ai';
2
+ import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
3
+
4
+ type SDKUserContentPart = SDKUserMessage['message']['content'][number];
5
+
6
+ interface StreamingSegment {
7
+ formatted: string;
8
+ }
9
+
10
+ const IMAGE_URL_WARNING = 'Image URLs are not supported by this provider; supply base64/data URLs.';
11
+ const IMAGE_CONVERSION_WARNING = 'Unable to convert image content; supply base64/data URLs.';
12
+
13
+ function normalizeBase64(base64: string): string {
14
+ return base64.replace(/\s+/g, '');
15
+ }
16
+
17
+ function isImageMimeType(mimeType?: string): boolean {
18
+ return typeof mimeType === 'string' && mimeType.trim().toLowerCase().startsWith('image/');
19
+ }
20
+
21
+ function createImageContent(mediaType: string, data: string): SDKUserContentPart | undefined {
22
+ const trimmedType = mediaType.trim();
23
+ const trimmedData = normalizeBase64(data.trim());
24
+
25
+ if (!trimmedType || !trimmedData) {
26
+ return undefined;
27
+ }
28
+
29
+ return {
30
+ type: 'image',
31
+ source: {
32
+ type: 'base64',
33
+ media_type: trimmedType,
34
+ data: trimmedData,
35
+ },
36
+ } as SDKUserContentPart;
37
+ }
38
+
39
+ function extractMimeType(candidate: unknown): string | undefined {
40
+ if (typeof candidate === 'string' && candidate.trim()) {
41
+ return candidate.trim();
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ function parseObjectImage(
47
+ imageObj: Record<string, unknown>,
48
+ fallbackMimeType?: string
49
+ ): SDKUserContentPart | undefined {
50
+ const data = typeof imageObj.data === 'string' ? imageObj.data : undefined;
51
+ const mimeType = extractMimeType(
52
+ imageObj.mimeType ?? imageObj.mediaType ?? imageObj.media_type ?? fallbackMimeType
53
+ );
54
+ if (!data || !mimeType) {
55
+ return undefined;
56
+ }
57
+ return createImageContent(mimeType, data);
58
+ }
59
+
60
+ function parseStringImage(
61
+ value: string,
62
+ fallbackMimeType?: string
63
+ ): { content?: SDKUserContentPart; warning?: string } {
64
+ const trimmed = value.trim();
65
+
66
+ if (/^https?:\/\//i.test(trimmed)) {
67
+ return { warning: IMAGE_URL_WARNING };
68
+ }
69
+
70
+ const dataUrlMatch = trimmed.match(/^data:([^;]+);base64,(.+)$/i);
71
+ if (dataUrlMatch) {
72
+ const [, mediaType, data] = dataUrlMatch;
73
+ const content = createImageContent(mediaType, data);
74
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
75
+ }
76
+
77
+ const base64Match = trimmed.match(/^base64:([^,]+),(.+)$/i);
78
+ if (base64Match) {
79
+ const [, explicitMimeType, data] = base64Match;
80
+ const content = createImageContent(explicitMimeType, data);
81
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
82
+ }
83
+
84
+ if (fallbackMimeType) {
85
+ const content = createImageContent(fallbackMimeType, trimmed);
86
+ if (content) {
87
+ return { content };
88
+ }
89
+ }
90
+
91
+ return { warning: IMAGE_CONVERSION_WARNING };
92
+ }
93
+
94
+ function parseImagePart(part: unknown): { content?: SDKUserContentPart; warning?: string } {
95
+ if (!part || typeof part !== 'object') {
96
+ return { warning: IMAGE_CONVERSION_WARNING };
97
+ }
98
+
99
+ const imageValue = (part as { image?: unknown }).image;
100
+ const mimeType = extractMimeType((part as { mimeType?: unknown }).mimeType);
101
+
102
+ if (typeof imageValue === 'string') {
103
+ return parseStringImage(imageValue, mimeType);
104
+ }
105
+
106
+ if (imageValue && typeof imageValue === 'object') {
107
+ const content = parseObjectImage(imageValue as Record<string, unknown>, mimeType);
108
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
109
+ }
110
+
111
+ return { warning: IMAGE_CONVERSION_WARNING };
112
+ }
113
+
114
+ function convertBinaryToBase64(data: Uint8Array | ArrayBuffer): string | undefined {
115
+ if (typeof Buffer !== 'undefined') {
116
+ const buffer =
117
+ data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(new Uint8Array(data));
118
+ return buffer.toString('base64');
119
+ }
120
+
121
+ if (typeof btoa === 'function') {
122
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
123
+ let binary = '';
124
+ const chunkSize = 0x8000;
125
+ for (let i = 0; i < bytes.length; i += chunkSize) {
126
+ const chunk = bytes.subarray(i, i + chunkSize);
127
+ binary += String.fromCharCode(...chunk);
128
+ }
129
+ return btoa(binary);
130
+ }
131
+
132
+ return undefined;
133
+ }
134
+
135
+ type FileLikePart = {
136
+ mediaType?: unknown;
137
+ mimeType?: unknown;
138
+ data?: unknown;
139
+ };
140
+
141
+ function parseFilePart(part: FileLikePart): { content?: SDKUserContentPart; warning?: string } {
142
+ const mimeType = extractMimeType(part.mediaType ?? part.mimeType);
143
+ if (!mimeType || !isImageMimeType(mimeType)) {
144
+ return {};
145
+ }
146
+
147
+ const data = part.data;
148
+ if (typeof data === 'string') {
149
+ const content = createImageContent(mimeType, data);
150
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
151
+ }
152
+
153
+ if (
154
+ data instanceof Uint8Array ||
155
+ (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer)
156
+ ) {
157
+ const base64 = convertBinaryToBase64(data);
158
+ if (!base64) {
159
+ return { warning: IMAGE_CONVERSION_WARNING };
160
+ }
161
+ const content = createImageContent(mimeType, base64);
162
+ return content ? { content } : { warning: IMAGE_CONVERSION_WARNING };
163
+ }
164
+
165
+ return { warning: IMAGE_CONVERSION_WARNING };
166
+ }
167
+
168
+ /**
169
+ * Converts AI SDK prompt format to Claude Code SDK message format.
170
+ * Handles system prompts, user messages, assistant responses, and tool interactions.
171
+ *
172
+ * @param prompt - The AI SDK prompt containing messages
173
+ * @returns An object containing the formatted message prompt and optional system prompt
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const { messagesPrompt } = convertToClaudeCodeMessages(
178
+ * [{ role: 'user', content: 'Hello!' }]
179
+ * );
180
+ * ```
181
+ *
182
+ * @remarks
183
+ * - Image parts are collected for streaming input; unsupported variants produce warnings
184
+ * - Tool calls are simplified to "[Tool calls made]" notation
185
+ * - JSON schema enforcement is handled natively by the SDK's outputFormat option (v0.1.45+)
186
+ */
187
+ export function convertToClaudeCodeMessages(prompt: readonly ModelMessage[]): {
188
+ messagesPrompt: string;
189
+ systemPrompt?: string;
190
+ warnings?: string[];
191
+ streamingContentParts: SDKUserMessage['message']['content'];
192
+ hasImageParts: boolean;
193
+ } {
194
+ const messages: string[] = [];
195
+ const warnings: string[] = [];
196
+ let systemPrompt: string | undefined;
197
+ const streamingSegments: StreamingSegment[] = [];
198
+ const imageMap = new Map<number, SDKUserContentPart[]>();
199
+ let hasImageParts = false;
200
+
201
+ const addSegment = (formatted: string): number => {
202
+ streamingSegments.push({ formatted });
203
+ return streamingSegments.length - 1;
204
+ };
205
+
206
+ const addImageForSegment = (segmentIndex: number, content: SDKUserContentPart): void => {
207
+ hasImageParts = true;
208
+ if (!imageMap.has(segmentIndex)) {
209
+ imageMap.set(segmentIndex, []);
210
+ }
211
+ imageMap.get(segmentIndex)?.push(content);
212
+ };
213
+
214
+ for (const message of prompt) {
215
+ switch (message.role) {
216
+ case 'system':
217
+ systemPrompt = message.content;
218
+ if (typeof message.content === 'string' && message.content.trim().length > 0) {
219
+ addSegment(message.content);
220
+ } else {
221
+ addSegment('');
222
+ }
223
+ break;
224
+
225
+ case 'user':
226
+ if (typeof message.content === 'string') {
227
+ messages.push(message.content);
228
+ addSegment(`Human: ${message.content}`);
229
+ } else {
230
+ // Handle multi-part content
231
+ const textParts = message.content
232
+ .filter((part) => part.type === 'text')
233
+ .map((part) => part.text)
234
+ .join('\n');
235
+
236
+ const segmentIndex = addSegment(textParts ? `Human: ${textParts}` : '');
237
+
238
+ if (textParts) {
239
+ messages.push(textParts);
240
+ }
241
+
242
+ for (const part of message.content) {
243
+ if (part.type === 'image') {
244
+ const { content, warning } = parseImagePart(part);
245
+ if (content) {
246
+ addImageForSegment(segmentIndex, content);
247
+ } else if (warning) {
248
+ warnings.push(warning);
249
+ }
250
+ } else if (part.type === 'file') {
251
+ const { content, warning } = parseFilePart(part);
252
+ if (content) {
253
+ addImageForSegment(segmentIndex, content);
254
+ } else if (warning) {
255
+ warnings.push(warning);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ break;
261
+
262
+ case 'assistant': {
263
+ let assistantContent = '';
264
+ if (typeof message.content === 'string') {
265
+ assistantContent = message.content;
266
+ } else {
267
+ const textParts = message.content
268
+ .filter((part) => part.type === 'text')
269
+ .map((part) => part.text)
270
+ .join('\n');
271
+
272
+ if (textParts) {
273
+ assistantContent = textParts;
274
+ }
275
+
276
+ // Handle tool calls if present
277
+ const toolCalls = message.content.filter((part) => part.type === 'tool-call');
278
+ if (toolCalls.length > 0) {
279
+ // For now, we'll just note that tool calls were made
280
+ assistantContent += `\n[Tool calls made]`;
281
+ }
282
+ }
283
+ const formattedAssistant = `Assistant: ${assistantContent}`;
284
+ messages.push(formattedAssistant);
285
+ addSegment(formattedAssistant);
286
+ break;
287
+ }
288
+
289
+ case 'tool':
290
+ // Tool results could be included in the conversation
291
+ // Filter out ToolApprovalResponse parts, only process ToolResultPart
292
+ for (const tool of message.content) {
293
+ if (tool.type === 'tool-approval-response') {
294
+ continue; // Skip approval responses
295
+ }
296
+ // Handle different ToolResultOutput types
297
+ let resultText: string;
298
+ const output = tool.output;
299
+ if (output.type === 'text' || output.type === 'error-text') {
300
+ resultText = output.value;
301
+ } else if (output.type === 'json' || output.type === 'error-json') {
302
+ resultText = JSON.stringify(output.value);
303
+ } else if (output.type === 'execution-denied') {
304
+ resultText = `[Execution denied${output.reason ? `: ${output.reason}` : ''}]`;
305
+ } else if (output.type === 'content') {
306
+ // Handle content array - extract text parts
307
+ resultText = output.value
308
+ .filter((part): part is { type: 'text'; text: string } => part.type === 'text')
309
+ .map((part) => part.text)
310
+ .join('\n');
311
+ } else {
312
+ resultText = '[Unknown output type]';
313
+ }
314
+ const formattedToolResult = `Tool Result (${tool.toolName}): ${resultText}`;
315
+ messages.push(formattedToolResult);
316
+ addSegment(formattedToolResult);
317
+ }
318
+ break;
319
+ }
320
+ }
321
+
322
+ // For the SDK, we need to provide a single prompt string
323
+ // Format the conversation history properly
324
+
325
+ // Combine system prompt with messages
326
+ let finalPrompt = '';
327
+
328
+ // Add system prompt at the beginning if present
329
+ if (systemPrompt) {
330
+ finalPrompt = systemPrompt;
331
+ }
332
+
333
+ if (messages.length > 0) {
334
+ // Format messages
335
+ const formattedMessages = [];
336
+ for (let i = 0; i < messages.length; i++) {
337
+ const msg = messages[i];
338
+ // Check if this is a user or assistant message based on content
339
+ if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) {
340
+ formattedMessages.push(msg);
341
+ } else {
342
+ // User messages
343
+ formattedMessages.push(`Human: ${msg}`);
344
+ }
345
+ }
346
+
347
+ // Combine system prompt with messages
348
+ if (finalPrompt) {
349
+ const joinedMessages = formattedMessages.join('\n\n');
350
+ finalPrompt = joinedMessages ? `${finalPrompt}\n\n${joinedMessages}` : finalPrompt;
351
+ } else {
352
+ finalPrompt = formattedMessages.join('\n\n');
353
+ }
354
+ }
355
+
356
+ // Build streaming parts including text and images
357
+ const streamingParts: SDKUserContentPart[] = [];
358
+ const imagePartsInOrder: SDKUserContentPart[] = [];
359
+
360
+ const appendImagesForIndex = (index: number) => {
361
+ const images = imageMap.get(index);
362
+ if (!images) {
363
+ return;
364
+ }
365
+ images.forEach((image) => {
366
+ streamingParts.push(image);
367
+ imagePartsInOrder.push(image);
368
+ });
369
+ };
370
+
371
+ if (streamingSegments.length > 0) {
372
+ let accumulatedText = '';
373
+ let emittedText = false;
374
+
375
+ const flushText = () => {
376
+ if (!accumulatedText) {
377
+ return;
378
+ }
379
+ streamingParts.push({ type: 'text', text: accumulatedText });
380
+ accumulatedText = '';
381
+ emittedText = true;
382
+ };
383
+
384
+ streamingSegments.forEach((segment, index) => {
385
+ const segmentText = segment.formatted;
386
+ if (segmentText) {
387
+ if (!accumulatedText) {
388
+ accumulatedText = emittedText ? `\n\n${segmentText}` : segmentText;
389
+ } else {
390
+ accumulatedText += `\n\n${segmentText}`;
391
+ }
392
+ }
393
+
394
+ if (imageMap.has(index)) {
395
+ flushText();
396
+ appendImagesForIndex(index);
397
+ }
398
+ });
399
+
400
+ flushText();
401
+ }
402
+
403
+ // Note: JSON schema enforcement is now handled natively by the SDK's outputFormat option (v0.1.45+)
404
+ // No prompt injection needed - structured outputs are guaranteed by the SDK
405
+
406
+ return {
407
+ messagesPrompt: finalPrompt,
408
+ systemPrompt,
409
+ ...(warnings.length > 0 && { warnings }),
410
+ streamingContentParts:
411
+ streamingParts.length > 0
412
+ ? (streamingParts as SDKUserMessage['message']['content'])
413
+ : ([
414
+ { type: 'text', text: finalPrompt },
415
+ ...imagePartsInOrder,
416
+ ] as SDKUserMessage['message']['content']),
417
+ hasImageParts,
418
+ };
419
+ }
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createAPICallError,
4
+ createAuthenticationError,
5
+ createTimeoutError,
6
+ isAuthenticationError,
7
+ isTimeoutError,
8
+ getErrorMetadata,
9
+ } from './errors.js';
10
+ import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
11
+
12
+ describe('Error Creation Functions', () => {
13
+ describe('createAPICallError', () => {
14
+ it('should create APICallError with message and details', () => {
15
+ const error = createAPICallError({
16
+ message: 'Test error',
17
+ exitCode: 1,
18
+ stderr: 'Command failed',
19
+ promptExcerpt: 'test prompt',
20
+ });
21
+
22
+ expect(error).toBeInstanceOf(APICallError);
23
+ expect(error.message).toBe('Test error');
24
+ expect(error.isRetryable).toBe(false);
25
+ expect(error.data).toEqual({
26
+ exitCode: 1,
27
+ stderr: 'Command failed',
28
+ promptExcerpt: 'test prompt',
29
+ code: undefined,
30
+ });
31
+ });
32
+
33
+ it('should handle optional parameters', () => {
34
+ const error = createAPICallError({
35
+ message: 'Minimal error',
36
+ });
37
+
38
+ expect(error).toBeInstanceOf(APICallError);
39
+ expect(error.message).toBe('Minimal error');
40
+ expect(error.requestBodyValues).toBeUndefined();
41
+ });
42
+
43
+ it('should set retryable flag', () => {
44
+ const error = createAPICallError({
45
+ message: 'Retryable error',
46
+ isRetryable: true,
47
+ });
48
+
49
+ expect(error.isRetryable).toBe(true);
50
+ });
51
+ });
52
+
53
+ describe('createAuthenticationError', () => {
54
+ it('should create LoadAPIKeyError for authentication', () => {
55
+ const error = createAuthenticationError({
56
+ message: 'Auth failed',
57
+ });
58
+
59
+ expect(error).toBeInstanceOf(LoadAPIKeyError);
60
+ expect(error.message).toBe('Auth failed');
61
+ });
62
+
63
+ it('should use default message when empty', () => {
64
+ const error = createAuthenticationError({
65
+ message: '',
66
+ });
67
+
68
+ expect(error.message).toBe(
69
+ 'Authentication failed. Please ensure Claude Code SDK is properly authenticated.'
70
+ );
71
+ });
72
+ });
73
+
74
+ describe('createTimeoutError', () => {
75
+ it('should create retryable APICallError for timeout', () => {
76
+ const error = createTimeoutError({
77
+ message: 'Request timed out after 2 minutes',
78
+ timeoutMs: 120000,
79
+ promptExcerpt: 'test prompt',
80
+ });
81
+
82
+ expect(error).toBeInstanceOf(APICallError);
83
+ expect(error.message).toBe('Request timed out after 2 minutes');
84
+ expect(error.isRetryable).toBe(true);
85
+ expect(error.data).toMatchObject({
86
+ code: 'TIMEOUT',
87
+ timeoutMs: 120000,
88
+ promptExcerpt: 'test prompt',
89
+ });
90
+ });
91
+
92
+ it('should work without prompt excerpt', () => {
93
+ const error = createTimeoutError({
94
+ message: 'Timeout',
95
+ timeoutMs: 60000,
96
+ });
97
+
98
+ expect(error.requestBodyValues).toBeUndefined();
99
+ expect((error.data as any).timeoutMs).toBe(60000);
100
+ });
101
+ });
102
+ });
103
+
104
+ describe('Error Detection Functions', () => {
105
+ describe('isAuthenticationError', () => {
106
+ it('should detect LoadAPIKeyError', () => {
107
+ const error = new LoadAPIKeyError({ message: 'Auth failed' });
108
+ expect(isAuthenticationError(error)).toBe(true);
109
+ });
110
+
111
+ it('should detect APICallError with exit code 401', () => {
112
+ const error = new APICallError({
113
+ message: 'Unauthorized',
114
+ url: 'test-url',
115
+ requestBodyValues: {},
116
+ isRetryable: false,
117
+ data: { exitCode: 401 },
118
+ });
119
+ expect(isAuthenticationError(error)).toBe(true);
120
+ });
121
+
122
+ it('should return false for other errors', () => {
123
+ expect(isAuthenticationError(new Error('Generic error'))).toBe(false);
124
+ expect(
125
+ isAuthenticationError(
126
+ new APICallError({
127
+ message: 'Not auth',
128
+ url: 'test-url',
129
+ requestBodyValues: {},
130
+ isRetryable: false,
131
+ data: { exitCode: 1 },
132
+ })
133
+ )
134
+ ).toBe(false);
135
+ expect(isAuthenticationError(null)).toBe(false);
136
+ });
137
+ });
138
+
139
+ describe('isTimeoutError', () => {
140
+ it('should detect APICallError with TIMEOUT code', () => {
141
+ const error = new APICallError({
142
+ message: 'Timeout',
143
+ url: 'test-url',
144
+ requestBodyValues: {},
145
+ isRetryable: true,
146
+ data: { code: 'TIMEOUT' },
147
+ });
148
+ expect(isTimeoutError(error)).toBe(true);
149
+ });
150
+
151
+ it('should return false for non-timeout errors', () => {
152
+ expect(isTimeoutError(new Error('Not timeout'))).toBe(false);
153
+ expect(
154
+ isTimeoutError(
155
+ new APICallError({
156
+ message: 'Other error',
157
+ url: 'test-url',
158
+ requestBodyValues: {},
159
+ isRetryable: false,
160
+ data: { code: 'OTHER' },
161
+ })
162
+ )
163
+ ).toBe(false);
164
+ expect(isTimeoutError(null)).toBe(false);
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('getErrorMetadata', () => {
170
+ it('should extract metadata from APICallError', () => {
171
+ const error = new APICallError({
172
+ message: 'API call failed',
173
+ url: 'test-url',
174
+ requestBodyValues: {},
175
+ isRetryable: false,
176
+ data: {
177
+ exitCode: 1,
178
+ stderr: 'error output',
179
+ code: 'ENOENT',
180
+ custom: 'data',
181
+ },
182
+ });
183
+
184
+ const metadata = getErrorMetadata(error);
185
+
186
+ expect(metadata).toEqual({
187
+ exitCode: 1,
188
+ stderr: 'error output',
189
+ code: 'ENOENT',
190
+ custom: 'data',
191
+ });
192
+ });
193
+
194
+ it('should return undefined for non-APICallError', () => {
195
+ const regularError = new Error('Regular error');
196
+ expect(getErrorMetadata(regularError)).toBeUndefined();
197
+
198
+ const customError = { message: 'Custom error' };
199
+ expect(getErrorMetadata(customError)).toBeUndefined();
200
+ });
201
+
202
+ it('should handle APICallError without data', () => {
203
+ const error = new APICallError({
204
+ message: 'API call failed',
205
+ url: 'test-url',
206
+ requestBodyValues: {},
207
+ isRetryable: false,
208
+ });
209
+
210
+ const metadata = getErrorMetadata(error);
211
+ expect(metadata).toBeUndefined();
212
+ });
213
+ });