@devpad/api 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,407 @@
1
+ import { ApiClient as HttpClient } from "./request";
2
+ import { wrap } from "./result";
3
+ /**
4
+ * API client with Result-wrapped operations for clean error handling
5
+ * All methods return Result<T, name> types with context-aware property names
6
+ */
7
+ export class ApiClient {
8
+ constructor(options) {
9
+ /**
10
+ * Auth namespace with Result-wrapped operations
11
+ */
12
+ this.auth = {
13
+ /**
14
+ * Get current session information
15
+ */
16
+ session: () => wrap(() => this.clients.auth.get("/auth/session"), "session"),
17
+ /**
18
+ * Login (redirect to OAuth)
19
+ */
20
+ login: () => wrap(() => this.clients.auth.get("/auth/login"), "result"),
21
+ /**
22
+ * Logout
23
+ */
24
+ logout: () => wrap(() => this.clients.auth.get("/auth/logout"), "result"),
25
+ /**
26
+ * API key management
27
+ */
28
+ keys: {
29
+ /**
30
+ * List all API keys
31
+ */
32
+ list: () => wrap(() => this.clients.auth.get("/auth/keys"), "keys"),
33
+ /**
34
+ * Generate a new API key
35
+ */
36
+ create: (name) => wrap(() => this.clients.auth.post("/auth/keys", { body: name ? { name } : {} }), "key"),
37
+ /**
38
+ * Revoke an API key
39
+ */
40
+ revoke: (key_id) => wrap(() => this.clients.auth.delete(`/auth/keys/${key_id}`), "result"),
41
+ /**
42
+ * Remove an API key (alias for revoke)
43
+ */
44
+ remove: (key_id) => wrap(() => this.clients.auth.delete(`/auth/keys/${key_id}`), "result"),
45
+ },
46
+ };
47
+ /**
48
+ * Projects namespace with Result-wrapped operations
49
+ */
50
+ this.projects = {
51
+ /**
52
+ * List projects with optional filtering
53
+ */
54
+ list: (filters) => wrap(() => this.clients.projects.get(filters?.private === false ? "/projects/public" : "/projects"), "projects"),
55
+ /**
56
+ * Get project map
57
+ */
58
+ map: async (filters) => wrap(async () => {
59
+ const { projects, error } = await this.projects.list(filters);
60
+ if (error)
61
+ throw new Error(error.message);
62
+ return projects.reduce((acc, project) => {
63
+ acc[project.project_id] = project;
64
+ return acc;
65
+ }, {});
66
+ }, "project_map"),
67
+ /**
68
+ * Get project by ID
69
+ */
70
+ find: (id) => wrap(() => this.clients.projects.get("/projects", { query: { id } }), "project"),
71
+ /**
72
+ * Get project by name
73
+ */
74
+ getByName: (name) => wrap(() => this.clients.projects.get("/projects", { query: { name } }), "project"),
75
+ /**
76
+ * Get project by ID (throws if not found)
77
+ */
78
+ getById: (id) => wrap(() => this.clients.projects.get("/projects", { query: { id } }), "project"),
79
+ /**
80
+ * Create a new project
81
+ */
82
+ create: (data) => wrap(() => this.clients.projects.patch("/projects", { body: data }), "project"),
83
+ /**
84
+ * Update an existing project
85
+ */
86
+ update: async (idOrData, changes) => wrap(async () => {
87
+ // Handle backward compatibility: update(data)
88
+ if (typeof idOrData === "object" && idOrData.id) {
89
+ return this.clients.projects.patch("/projects", { body: idOrData });
90
+ }
91
+ // Handle new clean interface: update(id, changes)
92
+ const id = idOrData;
93
+ if (!changes) {
94
+ throw new Error("Changes parameter required for update");
95
+ }
96
+ // Fetch the existing project to get current values
97
+ const { project, error } = await this.projects.find(id);
98
+ if (error)
99
+ throw new Error(error.message);
100
+ if (!project)
101
+ throw new Error(`Project with id ${id} not found`);
102
+ // Merge changes with existing project data
103
+ const updateData = {
104
+ id: project.id,
105
+ project_id: project.project_id,
106
+ owner_id: project.owner_id,
107
+ name: project.name,
108
+ description: project.description,
109
+ specification: project.specification,
110
+ repo_url: project.repo_url,
111
+ repo_id: project.repo_id,
112
+ icon_url: project.icon_url,
113
+ status: project.status,
114
+ deleted: project.deleted,
115
+ link_url: project.link_url,
116
+ link_text: project.link_text,
117
+ visibility: project.visibility,
118
+ current_version: project.current_version,
119
+ ...changes,
120
+ };
121
+ return this.clients.projects.patch("/projects", { body: updateData });
122
+ }, "project"),
123
+ /**
124
+ * Project configuration operations
125
+ */
126
+ config: {
127
+ /**
128
+ * Get project configuration
129
+ */
130
+ load: (project_id) => wrap(() => this.clients.projects.get("/projects/config", { query: { project_id } }), "config"),
131
+ /**
132
+ * Save project configuration
133
+ */
134
+ save: (request) => wrap(() => this.clients.projects.patch("/projects/save_config", { body: request }), "result"),
135
+ },
136
+ /**
137
+ * Scanning operations
138
+ */
139
+ scan: {
140
+ /**
141
+ * Update scan status
142
+ */
143
+ updateStatus: (project_id, data) => wrap(() => this.clients.projects.post(`/projects/${project_id}/scan/status`, { body: data }), "result"),
144
+ },
145
+ /**
146
+ * Get project history
147
+ */
148
+ history: (project_id) => wrap(() => this.clients.projects.get(`/projects/${project_id}/history`), "history"),
149
+ /**
150
+ * Legacy methods (keeping for compatibility)
151
+ */
152
+ upsert: (data) => wrap(() => this.clients.projects.patch("/projects", { body: data }), "project"),
153
+ /**
154
+ * Fetch project specification from GitHub
155
+ */
156
+ specification: (project_id) => wrap(() => this.clients.projects.get("/projects/fetch_spec", { query: { project_id } }), "specification"),
157
+ /**
158
+ * Delete project (soft delete)
159
+ */
160
+ deleteProject: (project) => wrap(() => this.clients.projects.patch("/projects", { body: { ...project, deleted: true } }), "result"),
161
+ };
162
+ /**
163
+ * Milestones namespace with Result-wrapped operations
164
+ */
165
+ this.milestones = {
166
+ /**
167
+ * List milestones for authenticated user
168
+ */
169
+ list: () => wrap(() => this.clients.milestones.get("/milestones"), "milestones"),
170
+ /**
171
+ * Get milestones by project ID
172
+ */
173
+ getByProject: (project_id) => wrap(() => this.clients.milestones.get(`/projects/${project_id}/milestones`), "milestones"),
174
+ /**
175
+ * Get milestone by ID
176
+ */
177
+ find: (id) => wrap(async () => {
178
+ try {
179
+ return await this.clients.milestones.get(`/milestones/${id}`);
180
+ }
181
+ catch (error) {
182
+ return null;
183
+ }
184
+ }, "milestone"),
185
+ /**
186
+ * Create new milestone
187
+ */
188
+ create: (data) => wrap(() => this.clients.milestones.post("/milestones", { body: data }), "milestone"),
189
+ /**
190
+ * Update milestone
191
+ */
192
+ update: async (id, data) => wrap(async () => {
193
+ // Fetch the existing milestone to get required fields
194
+ const { milestone, error } = await this.milestones.find(id);
195
+ if (error)
196
+ throw new Error(error.message);
197
+ if (!milestone)
198
+ throw new Error(`Milestone with id ${id} not found`);
199
+ // Merge changes with existing milestone data
200
+ const updateData = {
201
+ id: milestone.id,
202
+ project_id: milestone.project_id,
203
+ name: data.name ?? milestone.name,
204
+ description: data.description ?? milestone.description,
205
+ target_time: data.target_time ?? milestone.target_time,
206
+ target_version: data.target_version ?? milestone.target_version,
207
+ };
208
+ return this.clients.milestones.patch(`/milestones/${id}`, { body: updateData });
209
+ }, "milestone"),
210
+ /**
211
+ * Delete milestone (soft delete)
212
+ */
213
+ delete: (id) => wrap(() => this.clients.milestones.delete(`/milestones/${id}`), "result"),
214
+ /**
215
+ * Get goals for a milestone
216
+ */
217
+ goals: (id) => wrap(() => this.clients.milestones.get(`/milestones/${id}/goals`), "goals"),
218
+ };
219
+ /**
220
+ * Goals namespace with Result-wrapped operations
221
+ */
222
+ this.goals = {
223
+ /**
224
+ * List goals for authenticated user
225
+ */
226
+ list: () => wrap(() => this.clients.goals.get("/goals"), "goals"),
227
+ /**
228
+ * Get goal by ID
229
+ */
230
+ find: (id) => wrap(() => this.clients.goals.get(`/goals/${id}`), "goal"),
231
+ /**
232
+ * Create new goal
233
+ */
234
+ create: (data) => wrap(() => this.clients.goals.post("/goals", { body: data }), "goal"),
235
+ /**
236
+ * Update goal
237
+ */
238
+ update: async (id, data) => wrap(async () => {
239
+ // Fetch the existing goal to get required fields
240
+ const { goal, error } = await this.goals.find(id);
241
+ if (error)
242
+ throw new Error(error.message);
243
+ if (!goal)
244
+ throw new Error(`Goal with id ${id} not found`);
245
+ // Merge changes with existing goal data
246
+ const updateData = {
247
+ id: goal.id,
248
+ milestone_id: goal.milestone_id,
249
+ name: data.name ?? goal.name,
250
+ description: data.description ?? goal.description,
251
+ target_time: data.target_time ?? goal.target_time,
252
+ };
253
+ return this.clients.goals.patch(`/goals/${id}`, { body: updateData });
254
+ }, "goal"),
255
+ /**
256
+ * Delete goal (soft delete)
257
+ */
258
+ delete: (id) => wrap(() => this.clients.goals.delete(`/goals/${id}`), "result"),
259
+ };
260
+ /**
261
+ * Tasks namespace with Result-wrapped operations
262
+ */
263
+ this.tasks = {
264
+ /**
265
+ * List tasks with optional filtering
266
+ */
267
+ list: (filters) => wrap(() => {
268
+ const query = {};
269
+ if (filters?.project_id)
270
+ query.project = filters.project_id;
271
+ if (filters?.tag_id)
272
+ query.tag = filters.tag_id;
273
+ return this.clients.tasks.get("/tasks", Object.keys(query).length > 0 ? { query } : {});
274
+ }, "tasks"),
275
+ /**
276
+ * Get task by ID
277
+ */
278
+ find: (id) => wrap(() => this.clients.tasks.get("/tasks", { query: { id } }), "task"),
279
+ /**
280
+ * Get tasks by project ID
281
+ */
282
+ getByProject: (project_id) => wrap(() => this.clients.tasks.get(`/tasks`, { query: { project: project_id } }), "tasks"),
283
+ /**
284
+ * Create a new task
285
+ */
286
+ create: (data) => wrap(() => this.clients.tasks.patch("/tasks", { body: data }), "task"),
287
+ /**
288
+ * Update an existing task
289
+ */
290
+ update: async (id, changes) => wrap(async () => {
291
+ // Fetch existing task to merge changes
292
+ const { task, error } = await this.tasks.find(id);
293
+ if (error)
294
+ throw new Error(error.message);
295
+ if (!task)
296
+ throw new Error(`Task with id ${id} not found`);
297
+ const updateData = {
298
+ id,
299
+ title: task.task.title,
300
+ summary: task.task.summary,
301
+ description: task.task.description,
302
+ progress: task.task.progress,
303
+ visibility: task.task.visibility,
304
+ start_time: task.task.start_time,
305
+ end_time: task.task.end_time,
306
+ priority: task.task.priority,
307
+ owner_id: task.task.owner_id,
308
+ project_id: task.task.project_id,
309
+ ...changes,
310
+ };
311
+ return this.clients.tasks.patch("/tasks", { body: updateData });
312
+ }, "task"),
313
+ /**
314
+ * Upsert task (create or update)
315
+ */
316
+ upsert: (data) => wrap(() => this.clients.tasks.patch("/tasks", { body: data }), "task"),
317
+ /**
318
+ * Save tags for tasks
319
+ */
320
+ saveTags: (data) => wrap(() => this.clients.tasks.post("/tasks/save_tags", { body: data }), "result"),
321
+ /**
322
+ * Delete task (soft delete)
323
+ */
324
+ deleteTask: (task) => wrap(() => this.clients.tasks.patch("/tasks", { body: { ...task.task, deleted: true } }), "result"),
325
+ /**
326
+ * Task history operations
327
+ */
328
+ history: {
329
+ /**
330
+ * Get task history by task ID
331
+ */
332
+ get: (task_id) => wrap(() => this.clients.tasks.get(`/tasks/history/${task_id}`), "history"),
333
+ },
334
+ };
335
+ /**
336
+ * Tags namespace with Result-wrapped operations
337
+ */
338
+ this.tags = {
339
+ /**
340
+ * List tags for authenticated user
341
+ */
342
+ list: () => wrap(() => this.clients.tags.get("/tags"), "tags"),
343
+ };
344
+ /**
345
+ * GitHub namespace with Result-wrapped operations
346
+ */
347
+ this.github = {
348
+ /**
349
+ * List repositories for authenticated user
350
+ */
351
+ repos: () => wrap(() => this.clients.github.get("/repos"), "repos"),
352
+ /**
353
+ * List branches for a GitHub repository
354
+ */
355
+ branches: (owner, repo) => wrap(() => this.clients.github.get(`/repos/${owner}/${repo}/branches`), "branches"),
356
+ };
357
+ /**
358
+ * User namespace with Result-wrapped operations
359
+ */
360
+ this.user = {
361
+ /**
362
+ * Get user activity history
363
+ */
364
+ history: () => wrap(() => this.clients.auth.get("/user/history"), "history"),
365
+ /**
366
+ * Update user preferences
367
+ */
368
+ preferences: (data) => wrap(() => this.clients.auth.patch("/user/preferences", { body: data }), "result"),
369
+ };
370
+ const v0_base_url = options.base_url || "http://localhost:4321/api/v0";
371
+ this._api_key = options.api_key;
372
+ this._auth_mode = options.auth_mode || (options.api_key.startsWith("jwt:") ? "session" : "key");
373
+ // Create category-specific HTTP clients
374
+ const clientOptions = {
375
+ base_url: v0_base_url,
376
+ api_key: options.api_key,
377
+ max_history_size: options.max_history_size,
378
+ };
379
+ this.clients = {
380
+ auth: new HttpClient({ ...clientOptions, category: "auth" }),
381
+ projects: new HttpClient({ ...clientOptions, category: "projects" }),
382
+ tasks: new HttpClient({ ...clientOptions, category: "tasks" }),
383
+ milestones: new HttpClient({ ...clientOptions, category: "milestones" }),
384
+ goals: new HttpClient({ ...clientOptions, category: "goals" }),
385
+ github: new HttpClient({ ...clientOptions, category: "github" }),
386
+ tags: new HttpClient({ ...clientOptions, category: "tags" }),
387
+ };
388
+ }
389
+ /**
390
+ * Get request history for debugging
391
+ */
392
+ history() {
393
+ return this.clients.projects.history();
394
+ }
395
+ /**
396
+ * Get the API key
397
+ */
398
+ getApiKey() {
399
+ return this._api_key;
400
+ }
401
+ /**
402
+ * Get the authentication mode
403
+ */
404
+ getAuthMode() {
405
+ return this._auth_mode;
406
+ }
407
+ }
@@ -0,0 +1,55 @@
1
+ import { ApiError, AuthenticationError, NetworkError } from "./errors";
2
+ /**
3
+ * Centralized error handling utilities to reduce duplication
4
+ * across API client methods and improve consistency
5
+ */
6
+ /**
7
+ * Standard HTTP status codes
8
+ */
9
+ export declare const HTTP_STATUS: {
10
+ readonly OK: 200;
11
+ readonly CREATED: 201;
12
+ readonly NO_CONTENT: 204;
13
+ readonly BAD_REQUEST: 400;
14
+ readonly UNAUTHORIZED: 401;
15
+ readonly FORBIDDEN: 403;
16
+ readonly NOT_FOUND: 404;
17
+ readonly INTERNAL_SERVER_ERROR: 500;
18
+ };
19
+ /**
20
+ * Handle response based on status code with consistent error types
21
+ */
22
+ export declare function handleHttpResponse(response: Response): void;
23
+ /**
24
+ * Parse and throw appropriate error from response text
25
+ */
26
+ export declare function handleResponseError(response: Response): Promise<never>;
27
+ /**
28
+ * Handle network errors consistently
29
+ */
30
+ export declare function handleNetworkError(error: unknown): never;
31
+ /**
32
+ * Type guard to check if error is an API error
33
+ */
34
+ export declare function isApiError(error: unknown): error is ApiError;
35
+ /**
36
+ * Type guard to check if error is an authentication error
37
+ */
38
+ export declare function isAuthenticationError(error: unknown): error is AuthenticationError;
39
+ /**
40
+ * Type guard to check if error is a network error
41
+ */
42
+ export declare function isNetworkError(error: unknown): error is NetworkError;
43
+ /**
44
+ * Parse Zod validation errors into user-friendly messages
45
+ */
46
+ export declare function parseZodErrors(errorMessage: string): string;
47
+ /**
48
+ * Get user-friendly error message from any error
49
+ */
50
+ export declare function getUserFriendlyErrorMessage(error: unknown): string;
51
+ /**
52
+ * Retry wrapper for API calls with exponential backoff
53
+ */
54
+ export declare function withRetry<T>(operation: () => Promise<T>, maxRetries?: number, baseDelay?: number): Promise<T>;
55
+ //# sourceMappingURL=error-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handlers.d.ts","sourceRoot":"","sources":["../src/error-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,YAAY,EAAmB,MAAM,UAAU,CAAC;AAExF;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;;;;CASd,CAAC;AAEX;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAc3D;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAmB5E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAGxD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAElF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAgF3D;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA0BlE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,GAAE,MAAU,EAAE,SAAS,GAAE,MAAa,GAAG,OAAO,CAAC,CAAC,CAAC,CA0B5H"}
@@ -0,0 +1,217 @@
1
+ import { ApiError, AuthenticationError, NetworkError, ValidationError } from "./errors";
2
+ /**
3
+ * Centralized error handling utilities to reduce duplication
4
+ * across API client methods and improve consistency
5
+ */
6
+ /**
7
+ * Standard HTTP status codes
8
+ */
9
+ export const HTTP_STATUS = {
10
+ OK: 200,
11
+ CREATED: 201,
12
+ NO_CONTENT: 204,
13
+ BAD_REQUEST: 400,
14
+ UNAUTHORIZED: 401,
15
+ FORBIDDEN: 403,
16
+ NOT_FOUND: 404,
17
+ INTERNAL_SERVER_ERROR: 500,
18
+ };
19
+ /**
20
+ * Handle response based on status code with consistent error types
21
+ */
22
+ export function handleHttpResponse(response) {
23
+ switch (response.status) {
24
+ case HTTP_STATUS.UNAUTHORIZED:
25
+ throw new AuthenticationError("Invalid or expired API key");
26
+ case HTTP_STATUS.NOT_FOUND:
27
+ throw new ApiError("Resource not found", { statusCode: HTTP_STATUS.NOT_FOUND });
28
+ case HTTP_STATUS.BAD_REQUEST:
29
+ // Will be handled by specific error text parsing
30
+ break;
31
+ default:
32
+ if (!response.ok) {
33
+ throw new ApiError(`Request failed: ${response.statusText}`, { statusCode: response.status });
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * Parse and throw appropriate error from response text
39
+ */
40
+ export async function handleResponseError(response) {
41
+ const errorText = await response.text();
42
+ const errorMessage = errorText || "Request failed";
43
+ // Try to parse structured error responses
44
+ let parsedError = null;
45
+ try {
46
+ parsedError = JSON.parse(errorText);
47
+ }
48
+ catch {
49
+ // Not JSON, use raw text
50
+ }
51
+ if (response.status === HTTP_STATUS.BAD_REQUEST && parsedError?.error?.name === "ZodError") {
52
+ // Enhanced: Create a more detailed ValidationError with the original Zod error info
53
+ const zodErrorDetails = parsedError.error?.issues ? JSON.stringify(parsedError.error) : errorMessage;
54
+ throw new ValidationError(zodErrorDetails);
55
+ }
56
+ throw new ApiError(errorMessage, { statusCode: response.status });
57
+ }
58
+ /**
59
+ * Handle network errors consistently
60
+ */
61
+ export function handleNetworkError(error) {
62
+ const message = error instanceof Error ? error.message : "Unknown network error";
63
+ throw new NetworkError(message);
64
+ }
65
+ /**
66
+ * Type guard to check if error is an API error
67
+ */
68
+ export function isApiError(error) {
69
+ return error instanceof ApiError;
70
+ }
71
+ /**
72
+ * Type guard to check if error is an authentication error
73
+ */
74
+ export function isAuthenticationError(error) {
75
+ return error instanceof AuthenticationError;
76
+ }
77
+ /**
78
+ * Type guard to check if error is a network error
79
+ */
80
+ export function isNetworkError(error) {
81
+ return error instanceof NetworkError;
82
+ }
83
+ /**
84
+ * Parse Zod validation errors into user-friendly messages
85
+ */
86
+ export function parseZodErrors(errorMessage) {
87
+ try {
88
+ // Try to parse as JSON first to get structured error info
89
+ let parsedError = null;
90
+ try {
91
+ parsedError = JSON.parse(errorMessage);
92
+ }
93
+ catch {
94
+ // If not JSON, try to extract Zod error details from the error message
95
+ const zodErrorMatch = errorMessage.match(/ZodError: (.+)/);
96
+ if (zodErrorMatch && zodErrorMatch[1]) {
97
+ try {
98
+ parsedError = JSON.parse(zodErrorMatch[1]);
99
+ }
100
+ catch {
101
+ // If still not JSON, try to extract from the message
102
+ const issuesMatch = errorMessage.match(/issues:\s*(\[.*\])/s);
103
+ if (issuesMatch && issuesMatch[1]) {
104
+ try {
105
+ parsedError = { issues: JSON.parse(issuesMatch[1]) };
106
+ }
107
+ catch {
108
+ // Fall back to basic parsing
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ if (parsedError?.issues && Array.isArray(parsedError.issues)) {
115
+ const friendlyMessages = parsedError.issues.map((issue) => {
116
+ const path = issue.path && issue.path.length > 0 ? issue.path.join(".") : "field";
117
+ const message = issue.message || "is invalid";
118
+ // Handle common validation types with friendly messages
119
+ switch (issue.code) {
120
+ case "invalid_type":
121
+ return `${path} must be a ${issue.expected} (received ${issue.received})`;
122
+ case "too_small":
123
+ if (issue.type === "string") {
124
+ return `${path} must be at least ${issue.minimum} characters long`;
125
+ }
126
+ if (issue.type === "number") {
127
+ return `${path} must be at least ${issue.minimum}`;
128
+ }
129
+ return `${path} is too small`;
130
+ case "too_big":
131
+ if (issue.type === "string") {
132
+ return `${path} must be no more than ${issue.maximum} characters long`;
133
+ }
134
+ if (issue.type === "number") {
135
+ return `${path} must be no more than ${issue.maximum}`;
136
+ }
137
+ return `${path} is too large`;
138
+ case "invalid_string":
139
+ if (issue.validation === "email") {
140
+ return `${path} must be a valid email address`;
141
+ }
142
+ if (issue.validation === "url") {
143
+ return `${path} must be a valid URL`;
144
+ }
145
+ if (issue.validation === "uuid") {
146
+ return `${path} must be a valid UUID`;
147
+ }
148
+ return `${path} format is invalid`;
149
+ case "custom":
150
+ return `${path}: ${message}`;
151
+ default:
152
+ return `${path}: ${message}`;
153
+ }
154
+ });
155
+ if (friendlyMessages.length === 1) {
156
+ return friendlyMessages[0];
157
+ }
158
+ return `Validation failed:\n• ${friendlyMessages.join("\n• ")}`;
159
+ }
160
+ }
161
+ catch (e) {
162
+ // Fall back to original message if parsing fails
163
+ console.debug("Failed to parse Zod error:", e);
164
+ }
165
+ return errorMessage;
166
+ }
167
+ /**
168
+ * Get user-friendly error message from any error
169
+ */
170
+ export function getUserFriendlyErrorMessage(error) {
171
+ if (isAuthenticationError(error)) {
172
+ return "Please check your API key and try again";
173
+ }
174
+ if (isNetworkError(error)) {
175
+ return "Network connection issue. Please try again";
176
+ }
177
+ if (isApiError(error)) {
178
+ if (error.statusCode === HTTP_STATUS.NOT_FOUND) {
179
+ return "The requested resource was not found";
180
+ }
181
+ if (error.statusCode === HTTP_STATUS.BAD_REQUEST) {
182
+ return "Invalid request. Please check your data";
183
+ }
184
+ // Enhanced: Parse Zod validation errors for ValidationError types
185
+ if (error.code === "VALIDATION_ERROR" && error.message) {
186
+ return parseZodErrors(error.message);
187
+ }
188
+ return error.message || "An error occurred";
189
+ }
190
+ return "An unexpected error occurred";
191
+ }
192
+ /**
193
+ * Retry wrapper for API calls with exponential backoff
194
+ */
195
+ export async function withRetry(operation, maxRetries = 3, baseDelay = 1000) {
196
+ let lastError;
197
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
198
+ try {
199
+ return await operation();
200
+ }
201
+ catch (error) {
202
+ lastError = error;
203
+ // Don't retry authentication or validation errors
204
+ if (isAuthenticationError(error) || error instanceof ValidationError) {
205
+ throw error;
206
+ }
207
+ // Don't retry on final attempt
208
+ if (attempt === maxRetries) {
209
+ break;
210
+ }
211
+ // Exponential backoff
212
+ const delay = baseDelay * Math.pow(2, attempt - 1);
213
+ await new Promise(resolve => setTimeout(resolve, delay));
214
+ }
215
+ }
216
+ throw lastError;
217
+ }