@democratize-quality/mcp-server 1.2.0 → 1.2.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 (56) hide show
  1. package/cli.js +248 -0
  2. package/package.json +7 -5
  3. package/src/chatmodes//360/237/214/220 api-generator.chatmode.md" +409 -0
  4. package/src/chatmodes//360/237/214/220 api-healer.chatmode.md" +494 -0
  5. package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +954 -0
  6. package/src/config/environments/api-only.js +72 -0
  7. package/src/config/environments/development.js +73 -0
  8. package/src/config/environments/production.js +88 -0
  9. package/src/config/index.js +360 -0
  10. package/src/config/server.js +60 -0
  11. package/src/config/tools/api.js +86 -0
  12. package/src/config/tools/browser.js +109 -0
  13. package/src/config/tools/default.js +51 -0
  14. package/src/docs/Agent_README.md +310 -0
  15. package/src/docs/QUICK_REFERENCE.md +111 -0
  16. package/src/server.ts +234 -0
  17. package/src/services/browserService.js +344 -0
  18. package/src/skills/api-planning/SKILL.md +224 -0
  19. package/src/skills/test-execution/SKILL.md +777 -0
  20. package/src/skills/test-generation/SKILL.md +309 -0
  21. package/src/skills/test-healing/SKILL.md +405 -0
  22. package/src/tools/api/api-generator.js +1884 -0
  23. package/src/tools/api/api-healer.js +636 -0
  24. package/src/tools/api/api-planner.js +2617 -0
  25. package/src/tools/api/api-project-setup.js +332 -0
  26. package/src/tools/api/api-request.js +660 -0
  27. package/src/tools/api/api-session-report.js +1297 -0
  28. package/src/tools/api/api-session-status.js +414 -0
  29. package/src/tools/api/prompts/README.md +293 -0
  30. package/src/tools/api/prompts/generation-prompts.js +722 -0
  31. package/src/tools/api/prompts/healing-prompts.js +214 -0
  32. package/src/tools/api/prompts/index.js +44 -0
  33. package/src/tools/api/prompts/orchestrator.js +353 -0
  34. package/src/tools/api/prompts/validation-rules.js +358 -0
  35. package/src/tools/base/ToolBase.js +249 -0
  36. package/src/tools/base/ToolRegistry.js +288 -0
  37. package/src/tools/browser/advanced/browser-console.js +403 -0
  38. package/src/tools/browser/advanced/browser-dialog.js +338 -0
  39. package/src/tools/browser/advanced/browser-evaluate.js +356 -0
  40. package/src/tools/browser/advanced/browser-file.js +499 -0
  41. package/src/tools/browser/advanced/browser-keyboard.js +362 -0
  42. package/src/tools/browser/advanced/browser-mouse.js +351 -0
  43. package/src/tools/browser/advanced/browser-network.js +440 -0
  44. package/src/tools/browser/advanced/browser-pdf.js +426 -0
  45. package/src/tools/browser/advanced/browser-tabs.js +516 -0
  46. package/src/tools/browser/advanced/browser-wait.js +397 -0
  47. package/src/tools/browser/click.js +187 -0
  48. package/src/tools/browser/close.js +79 -0
  49. package/src/tools/browser/dom.js +89 -0
  50. package/src/tools/browser/launch.js +86 -0
  51. package/src/tools/browser/navigate.js +289 -0
  52. package/src/tools/browser/screenshot.js +370 -0
  53. package/src/tools/browser/type.js +193 -0
  54. package/src/tools/index.js +114 -0
  55. package/src/utils/agentInstaller.js +437 -0
  56. package/src/utils/browserHelpers.js +102 -0
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Copyright (C) 2025 Democratize Quality
3
+ *
4
+ * This file is part of Democratize Quality MCP Server.
5
+ *
6
+ * Democratize Quality MCP Server is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Affero General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * Democratize Quality MCP Server is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU Affero General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Affero General Public License
17
+ * along with Democratize Quality MCP Server. If not, see <https://www.gnu.org/licenses/>.
18
+ */
19
+
20
+ const ToolBase = require('../base/ToolBase');
21
+ const https = require('https');
22
+ const http = require('http');
23
+ const { URL } = require('url');
24
+
25
+ let z;
26
+ try {
27
+ // import Zod
28
+ const zod = require('zod');
29
+ z = zod.z || zod.default?.z || zod;
30
+ if (!z || typeof z.object !== 'function') {
31
+ throw new Error('Zod not properly loaded');
32
+ }
33
+ } catch (error) {
34
+ console.error('Failed to load Zod:', error.message);
35
+ // Fallback: create a simple validation function
36
+ z = {
37
+ object: (schema) => ({ parse: (data) => data }),
38
+ string: () => ({ optional: () => ({}) }),
39
+ enum: () => ({ optional: () => ({}) }),
40
+ record: () => ({ optional: () => ({}) }),
41
+ any: () => ({ optional: () => ({}) }),
42
+ number: () => ({ optional: () => ({}) }),
43
+ array: () => ({ optional: () => ({}) })
44
+ };
45
+ }
46
+
47
+ // Input schema for the API request tool
48
+ const chainStepSchema = z.object({
49
+ name: z.string(),
50
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).optional(),
51
+ url: z.string(),
52
+ headers: z.record(z.string()).optional(),
53
+ data: z.any().optional(),
54
+ expect: z.object({
55
+ status: z.number().optional(),
56
+ contentType: z.string().optional(),
57
+ body: z.any().optional(),
58
+ bodyRegex: z.string().optional()
59
+ }).optional(),
60
+ extract: z.record(z.string()).optional() // { varName: 'field' }
61
+ });
62
+
63
+ const apiRequestInputSchema = z.object({
64
+ sessionId: z.string().optional(), // New: session management
65
+ // Single-request legacy mode
66
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).optional(),
67
+ url: z.string().optional(),
68
+ headers: z.record(z.string()).optional(),
69
+ data: z.any().optional(),
70
+ expect: z.object({
71
+ status: z.number().optional(),
72
+ contentType: z.string().optional(),
73
+ body: z.any().optional(),
74
+ bodyRegex: z.string().optional()
75
+ }).optional(),
76
+ // Chaining mode
77
+ chain: z.array(chainStepSchema).optional()
78
+ });
79
+
80
+ // --- In-memory session store ---
81
+ const sessionStore = global.__API_SESSION_STORE__ || new Map();
82
+ global.__API_SESSION_STORE__ = sessionStore;
83
+
84
+ /**
85
+ * API Request Tool - Perform HTTP API requests with validation and session management
86
+ */
87
+ class ApiRequestTool extends ToolBase {
88
+ static definition = {
89
+ name: "api_request",
90
+ description: "Perform HTTP API requests with validation, session management, and request chaining capabilities for comprehensive API testing.",
91
+ input_schema: {
92
+ type: "object",
93
+ properties: {
94
+ sessionId: {
95
+ type: "string",
96
+ description: "Optional session ID for managing related requests"
97
+ },
98
+ method: {
99
+ type: "string",
100
+ enum: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
101
+ description: "HTTP method for the request"
102
+ },
103
+ url: {
104
+ type: "string",
105
+ description: "Target URL for the HTTP request"
106
+ },
107
+ headers: {
108
+ type: "object",
109
+ additionalProperties: { type: "string" },
110
+ description: "HTTP headers as key-value pairs"
111
+ },
112
+ data: {
113
+ description: "Request body data (JSON object, string, etc.)"
114
+ },
115
+ expect: {
116
+ type: "object",
117
+ properties: {
118
+ status: {
119
+ type: "number",
120
+ description: "Expected HTTP status code"
121
+ },
122
+ contentType: {
123
+ type: "string",
124
+ description: "Expected content type"
125
+ },
126
+ body: {
127
+ description: "Expected response body content"
128
+ },
129
+ bodyRegex: {
130
+ type: "string",
131
+ description: "Regex pattern to match against response body"
132
+ }
133
+ },
134
+ description: "Validation expectations for the response"
135
+ },
136
+ chain: {
137
+ type: "array",
138
+ items: {
139
+ type: "object",
140
+ properties: {
141
+ name: {
142
+ type: "string",
143
+ description: "Step name for reference in chaining"
144
+ },
145
+ method: {
146
+ type: "string",
147
+ enum: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
148
+ description: "HTTP method for this step"
149
+ },
150
+ url: {
151
+ type: "string",
152
+ description: "URL for this step (supports templating)"
153
+ },
154
+ headers: {
155
+ type: "object",
156
+ additionalProperties: { type: "string" },
157
+ description: "Headers for this step"
158
+ },
159
+ data: {
160
+ description: "Data for this step"
161
+ },
162
+ expect: {
163
+ type: "object",
164
+ properties: {
165
+ status: { type: "number" },
166
+ contentType: { type: "string" },
167
+ body: {},
168
+ bodyRegex: { type: "string" }
169
+ },
170
+ description: "Validation expectations"
171
+ },
172
+ extract: {
173
+ type: "object",
174
+ additionalProperties: { type: "string" },
175
+ description: "Fields to extract for use in subsequent steps"
176
+ }
177
+ },
178
+ required: ["name", "url"]
179
+ },
180
+ description: "Chain of requests to execute sequentially"
181
+ }
182
+ },
183
+ additionalProperties: false
184
+ }
185
+ };
186
+
187
+ constructor() {
188
+ super();
189
+ this.sessionStore = sessionStore;
190
+ }
191
+
192
+ /**
193
+ * Simple validation function as fallback if Zod fails
194
+ */
195
+ validateInput(input) {
196
+ // Basic validation
197
+ if (typeof input !== 'object' || input === null) {
198
+ throw new Error('Input must be an object');
199
+ }
200
+
201
+ // If chain is provided, validate chain structure
202
+ if (input.chain && Array.isArray(input.chain)) {
203
+ for (let i = 0; i < input.chain.length; i++) {
204
+ const step = input.chain[i];
205
+ if (!step.name || !step.url) {
206
+ throw new Error(`Chain step ${i}: missing required fields 'name' or 'url'`);
207
+ }
208
+ if (step.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(step.method)) {
209
+ throw new Error(`Chain step ${i}: invalid method '${step.method}'`);
210
+ }
211
+ }
212
+ }
213
+
214
+ // For single request mode, url is required if no chain
215
+ if (!input.chain && !input.url) {
216
+ throw new Error('URL is required for single request mode');
217
+ }
218
+
219
+ if (input.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(input.method)) {
220
+ throw new Error(`Invalid method '${input.method}'`);
221
+ }
222
+
223
+ return input;
224
+ }
225
+
226
+ async execute(parameters) {
227
+ try {
228
+ // Validate input - try Zod first, fallback to simple validation
229
+ let input;
230
+ try {
231
+ if (typeof apiRequestInputSchema.parse === 'function') {
232
+ input = apiRequestInputSchema.parse(parameters);
233
+ } else {
234
+ input = this.validateInput(parameters);
235
+ }
236
+ } catch (zodError) {
237
+ console.warn('Zod validation failed, using fallback validation:', zodError.message);
238
+ input = this.validateInput(parameters);
239
+ }
240
+
241
+ // --- Session Management ---
242
+ const uuid = () => {
243
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
244
+ return crypto.randomUUID();
245
+ // Simple pseudo-unique fallback: not cryptographically secure, but fine for session IDs
246
+ return 'session-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
247
+ };
248
+
249
+ const sessionId = input.sessionId || uuid();
250
+ if (!sessionStore.has(sessionId)) {
251
+ sessionStore.set(sessionId, {
252
+ sessionId,
253
+ startTime: new Date().toISOString(),
254
+ logs: [],
255
+ status: 'running'
256
+ });
257
+ }
258
+ const session = sessionStore.get(sessionId);
259
+
260
+ // --- API CHAINING SUPPORT ---
261
+ function renderTemplate(str, vars) {
262
+ if (typeof str !== 'string') return str;
263
+ return str.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
264
+ const [step, ...rest] = path.split('.');
265
+ let val = vars[step];
266
+ for (const p of rest)
267
+ val = val?.[p];
268
+ return val !== undefined ? String(val) : '';
269
+ });
270
+ }
271
+
272
+ function extractFields(obj, extract) {
273
+ const result = {};
274
+ if (!extract || typeof extract !== 'object') return result;
275
+
276
+ for (const [k, path] of Object.entries(extract)) {
277
+ if (typeof path !== 'string') continue;
278
+ const parts = path.split('.');
279
+ let val = obj;
280
+ for (const p of parts) {
281
+ val = val?.[p];
282
+ if (val === undefined) break;
283
+ }
284
+ result[k] = val;
285
+ }
286
+ return result;
287
+ }
288
+
289
+ // If 'chain' is present, execute steps sequentially
290
+ if (Array.isArray(input.chain)) {
291
+ const stepVars = {};
292
+ const results = [];
293
+
294
+ for (const step of input.chain) {
295
+ // Validate step has required fields
296
+ if (!step.name || !step.url) {
297
+ throw new Error(`Invalid chain step: missing name or url`);
298
+ }
299
+
300
+ // Render templates in url, headers, data
301
+ const url = renderTemplate(step.url, stepVars);
302
+ const headers = {};
303
+ for (const k in (step.headers || {})) {
304
+ headers[k] = renderTemplate(step.headers[k], stepVars);
305
+ }
306
+
307
+ let data = step.data;
308
+ if (typeof data === 'string') {
309
+ data = renderTemplate(data, stepVars);
310
+ } else if (typeof data === 'object' && data !== null) {
311
+ // Handle object data with template rendering
312
+ try {
313
+ data = JSON.parse(renderTemplate(JSON.stringify(data), stepVars));
314
+ } catch (e) {
315
+ // If JSON parsing fails, keep original data
316
+ console.warn('Failed to parse templated data, using original:', e.message);
317
+ }
318
+ }
319
+
320
+ // Execute request using Node.js built-in modules
321
+ const response = await this.makeHttpRequest(
322
+ step.method || 'GET',
323
+ url,
324
+ headers,
325
+ data
326
+ );
327
+
328
+ const status = response.statusCode;
329
+ const statusText = response.statusMessage || '';
330
+ const contentType = response.headers['content-type'] || '';
331
+ let responseBody = response.body;
332
+
333
+ if (contentType.includes('application/json')) {
334
+ try {
335
+ responseBody = JSON.parse(response.body);
336
+ } catch (e) {
337
+ // Keep as string if JSON parsing fails
338
+ }
339
+ }
340
+
341
+ // Validation
342
+ const expect = step.expect || {};
343
+ const validation = {
344
+ status: expect.status ? status === expect.status : true,
345
+ contentType: expect.contentType ? contentType.includes(expect.contentType) : true
346
+ };
347
+
348
+ let bodyValidation = { matched: true, reason: 'No body expectation set.' };
349
+ if (expect.body !== undefined) {
350
+ if (typeof responseBody === 'object' && responseBody !== null && typeof expect.body === 'object') {
351
+ bodyValidation.matched = Object.entries(expect.body).every(
352
+ ([k, v]) => JSON.stringify(responseBody[k]) === JSON.stringify(v)
353
+ );
354
+ bodyValidation.reason = bodyValidation.matched
355
+ ? 'Partial/exact body match succeeded.'
356
+ : 'Partial/exact body match failed.';
357
+ } else if (typeof expect.body === 'string') {
358
+ bodyValidation.matched = JSON.stringify(responseBody) === expect.body || responseBody === expect.body;
359
+ bodyValidation.reason = bodyValidation.matched
360
+ ? 'Exact string match succeeded.'
361
+ : 'Exact string match failed.';
362
+ } else {
363
+ bodyValidation.matched = false;
364
+ bodyValidation.reason = 'Body type mismatch.';
365
+ }
366
+ }
367
+ if (expect.bodyRegex) {
368
+ try {
369
+ const pattern = new RegExp(expect.bodyRegex);
370
+ const target = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
371
+ const regexMatch = pattern.test(target);
372
+ bodyValidation = {
373
+ matched: regexMatch,
374
+ reason: regexMatch ? 'Regex match succeeded.' : 'Regex match failed.'
375
+ };
376
+ } catch (regexError) {
377
+ bodyValidation = {
378
+ matched: false,
379
+ reason: `Invalid regex pattern: ${regexError.message}`
380
+ };
381
+ }
382
+ }
383
+
384
+ // Extract variables
385
+ const extracted = step.extract ? extractFields(responseBody, step.extract) : {};
386
+ // Add extracted variables directly to stepVars for template rendering
387
+ Object.assign(stepVars, extracted);
388
+ // Also store step results for reference (including all response data)
389
+ stepVars[step.name] = {
390
+ ...extracted,
391
+ body: responseBody,
392
+ status,
393
+ contentType,
394
+ // Add common response fields for easier access
395
+ id: responseBody?.id,
396
+ userId: responseBody?.userId,
397
+ title: responseBody?.title
398
+ };
399
+
400
+ // Record step result
401
+ results.push({
402
+ name: step.name,
403
+ status,
404
+ statusText,
405
+ contentType,
406
+ body: responseBody,
407
+ expectations: step.expect || {},
408
+ validation,
409
+ bodyValidation,
410
+ extracted
411
+ });
412
+
413
+ // Log to session
414
+ session.logs.push({
415
+ type: 'request',
416
+ request: {
417
+ method: step.method || 'GET',
418
+ url,
419
+ headers,
420
+ data
421
+ },
422
+ response: {
423
+ status,
424
+ statusText,
425
+ contentType,
426
+ body: responseBody
427
+ },
428
+ expectations: step.expect || {},
429
+ validation,
430
+ bodyValidation,
431
+ timestamp: new Date().toISOString()
432
+ });
433
+ }
434
+
435
+ // Log to session
436
+ session.logs.push({
437
+ type: 'chain',
438
+ steps: results,
439
+ timestamp: new Date().toISOString()
440
+ });
441
+ session.status = 'completed';
442
+
443
+ return {
444
+ content: [{
445
+ type: 'text',
446
+ text: JSON.stringify({ sessionId, results }, null, 2)
447
+ }]
448
+ };
449
+ }
450
+
451
+ // --- SINGLE REQUEST MODE (legacy) ---
452
+ const { method, url, headers, data, expect } = input;
453
+
454
+ // Validate required parameters for single request mode
455
+ if (!url)
456
+ throw new Error('URL is required for single request mode');
457
+
458
+ const response = await this.makeHttpRequest(
459
+ method || 'GET',
460
+ url,
461
+ headers,
462
+ data
463
+ );
464
+
465
+ const status = response.statusCode;
466
+ const statusText = response.statusMessage || '';
467
+ const contentType = response.headers['content-type'] || '';
468
+ let responseBody = response.body;
469
+
470
+ if (contentType.includes('application/json')) {
471
+ try {
472
+ responseBody = JSON.parse(response.body);
473
+ } catch (e) {
474
+ // Keep as string if JSON parsing fails
475
+ }
476
+ }
477
+
478
+ // Basic validation
479
+ const validation = {
480
+ status: expect?.status ? status === expect.status : true,
481
+ contentType: expect?.contentType ? contentType.includes(expect?.contentType) : true
482
+ };
483
+
484
+ // --- Enhanced Response Body Validation ---
485
+ let bodyValidation = { matched: true, reason: 'No body expectation set.' };
486
+ if (expect?.body !== undefined) {
487
+ if (typeof responseBody === 'object' && responseBody !== null && typeof expect.body === 'object') {
488
+ // Partial match: all keys/values in expect.body must be present in responseBody
489
+ bodyValidation.matched = Object.entries(expect.body).every(
490
+ ([k, v]) => JSON.stringify(responseBody[k]) === JSON.stringify(v)
491
+ );
492
+ bodyValidation.reason = bodyValidation.matched
493
+ ? 'Partial/exact body match succeeded.'
494
+ : 'Partial/exact body match failed.';
495
+ } else if (typeof expect.body === 'string') {
496
+ bodyValidation.matched = JSON.stringify(responseBody) === expect.body || responseBody === expect.body;
497
+ bodyValidation.reason = bodyValidation.matched
498
+ ? 'Exact string match succeeded.'
499
+ : 'Exact string match failed.';
500
+ } else {
501
+ bodyValidation.matched = false;
502
+ bodyValidation.reason = 'Body type mismatch.';
503
+ }
504
+ }
505
+ if (expect?.bodyRegex) {
506
+ try {
507
+ const pattern = new RegExp(expect.bodyRegex);
508
+ const target = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
509
+ const regexMatch = pattern.test(target);
510
+ bodyValidation = {
511
+ matched: regexMatch,
512
+ reason: regexMatch ? 'Regex match succeeded.' : 'Regex match failed.'
513
+ };
514
+ } catch (regexError) {
515
+ bodyValidation = {
516
+ matched: false,
517
+ reason: `Invalid regex pattern: ${regexError.message}`
518
+ };
519
+ }
520
+ }
521
+ // --- End Enhanced Validation ---
522
+
523
+ // Log to session
524
+ if (session && session.logs) {
525
+ session.logs.push({
526
+ type: 'single',
527
+ request: {
528
+ method: method || 'GET',
529
+ url,
530
+ headers,
531
+ data
532
+ },
533
+ response: {
534
+ status,
535
+ statusText,
536
+ contentType,
537
+ body: responseBody
538
+ },
539
+ expectations: expect || {},
540
+ validation,
541
+ bodyValidation,
542
+ timestamp: new Date().toISOString()
543
+ });
544
+ }
545
+
546
+ return {
547
+ content: [{
548
+ type: 'text',
549
+ text: JSON.stringify({
550
+ ok: validation.status && validation.contentType && bodyValidation.matched,
551
+ status,
552
+ statusText,
553
+ contentType,
554
+ body: responseBody,
555
+ validation,
556
+ bodyValidation
557
+ }, null, 2)
558
+ }]
559
+ };
560
+
561
+ } catch (error) {
562
+ // Error handling for API request execution
563
+ const errorMessage = error.message || 'Unknown error occurred';
564
+ console.error('ApiRequestTool execution error:', errorMessage);
565
+
566
+ return {
567
+ content: [{
568
+ type: 'text',
569
+ text: JSON.stringify({
570
+ error: true,
571
+ message: errorMessage,
572
+ stack: error.stack
573
+ }, null, 2)
574
+ }]
575
+ };
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Make HTTP request using Node.js built-in modules
581
+ */
582
+ async makeHttpRequest(method, urlString, headers = {}, data, timeout = 30000) {
583
+ return new Promise((resolve, reject) => {
584
+ let url;
585
+ try {
586
+ url = new URL(urlString);
587
+ } catch (urlError) {
588
+ reject(new Error(`Invalid URL: ${urlString}`));
589
+ return;
590
+ }
591
+
592
+ const isHttps = url.protocol === 'https:';
593
+ const lib = isHttps ? https : http;
594
+
595
+ const options = {
596
+ hostname: url.hostname,
597
+ port: url.port || (isHttps ? 443 : 80),
598
+ path: url.pathname + url.search,
599
+ method: method.toUpperCase(),
600
+ headers: {
601
+ 'User-Agent': 'Democratize-Quality-MCP/1.0',
602
+ ...headers
603
+ },
604
+ timeout
605
+ };
606
+
607
+ // Adding Content-Length for requests with body
608
+ if (data) {
609
+ const bodyString = typeof data === 'string' ? data : JSON.stringify(data);
610
+ options.headers['Content-Length'] = Buffer.byteLength(bodyString);
611
+
612
+ if (!options.headers['Content-Type']) {
613
+ options.headers['Content-Type'] = typeof data === 'object'
614
+ ? 'application/json'
615
+ : 'text/plain';
616
+ }
617
+ }
618
+
619
+ const req = lib.request(options, (res) => {
620
+ let body = '';
621
+
622
+ res.on('data', (chunk) => {
623
+ body += chunk;
624
+ });
625
+
626
+ res.on('end', () => {
627
+ resolve({
628
+ statusCode: res.statusCode,
629
+ headers: res.headers,
630
+ body: body
631
+ });
632
+ });
633
+ });
634
+
635
+ req.on('error', (error) => {
636
+ reject(new Error(`Request failed: ${error.message}`));
637
+ });
638
+
639
+ req.on('timeout', () => {
640
+ req.destroy();
641
+ reject(new Error(`Request timeout after ${timeout}ms`));
642
+ });
643
+
644
+ // Write back request body if present
645
+ if (data) {
646
+ try {
647
+ const bodyString = typeof data === 'string' ? data : JSON.stringify(data);
648
+ req.write(bodyString);
649
+ } catch (writeError) {
650
+ reject(new Error(`Failed to write request body: ${writeError.message}`));
651
+ return;
652
+ }
653
+ }
654
+
655
+ req.end();
656
+ });
657
+ }
658
+ }
659
+
660
+ module.exports = ApiRequestTool;