@velt-js/mcp-installer 0.1.0

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,288 @@
1
+ /**
2
+ * Velt Docs Fetcher
3
+ *
4
+ * Fetches Velt documentation from markdown (.md) URLs with fallback to Velt Docs MCP.
5
+ * For post-installation questions/troubleshooting, AI should query Velt Docs MCP directly.
6
+ *
7
+ * Source Priority:
8
+ * 1. Agent Skills Libraries (installed via `npx skills add velt-js/agent-skills`)
9
+ * - Referenced by name in plan output; AI editor loads them automatically
10
+ * 2. Docs URLs (docs.velt.dev markdown) - for features without skills coverage
11
+ * 3. Velt Docs MCP - only on explicit user follow-up questions
12
+ */
13
+
14
+ import { getDocUrl, getDocMarkdownUrl } from './velt-docs-urls.js';
15
+ import { queryVeltDocsMCP } from './velt-mcp-client.js';
16
+
17
+ /**
18
+ * Maps Velt features to their corresponding Agent Skill names.
19
+ * Skills are installed via `npx skills add velt-js/agent-skills` and
20
+ * loaded into the AI editor's context automatically.
21
+ *
22
+ * The MCP references these by name in plan output — it does NOT read skill files.
23
+ */
24
+ export const FEATURE_SKILL_MAP = {
25
+ // Setup / Provider / Auth / Document
26
+ setup: 'velt-setup-best-practices',
27
+ provider: 'velt-setup-best-practices',
28
+ auth: 'velt-setup-best-practices',
29
+ document: 'velt-setup-best-practices',
30
+
31
+ // Comments (all types)
32
+ comments: 'velt-comments-best-practices',
33
+
34
+ // CRDT (all editor types)
35
+ crdt: 'velt-crdt-best-practices',
36
+
37
+ // Notifications
38
+ notifications: 'velt-notifications-best-practices',
39
+
40
+ // Features WITHOUT skills coverage — use docs URLs
41
+ // presence: null,
42
+ // cursors: null,
43
+ // recorder: null,
44
+ };
45
+
46
+ /**
47
+ * Returns the Agent Skill name for a given feature, or null if no skill covers it.
48
+ *
49
+ * @param {string} feature - Feature name (comments, crdt, notifications, presence, etc.)
50
+ * @param {string} [subtype] - Optional subtype (freestyle, tiptap, etc.) — unused for mapping but kept for API consistency
51
+ * @returns {string|null} Skill name or null
52
+ */
53
+ export function getSkillForFeature(feature, subtype = null) {
54
+ return FEATURE_SKILL_MAP[feature] || null;
55
+ }
56
+
57
+ /**
58
+ * Returns an array of { feature, skillName } objects for all features that have skills,
59
+ * plus { feature, docsUrl } for features that fall back to docs.
60
+ *
61
+ * @param {string[]} features - Array of feature names
62
+ * @param {Object} [options] - Options
63
+ * @param {string} [options.commentType] - Comment subtype
64
+ * @param {string} [options.crdtEditorType] - CRDT editor subtype
65
+ * @returns {Array<{feature: string, skillName?: string, docsUrl?: string}>}
66
+ */
67
+ export function getSkillReferences(features, options = {}) {
68
+ const { commentType, crdtEditorType } = options;
69
+ const refs = [];
70
+
71
+ // Always include setup skill
72
+ refs.push({
73
+ feature: 'setup',
74
+ skillName: FEATURE_SKILL_MAP.setup,
75
+ description: 'VeltProvider setup, authentication, document identity, project structure',
76
+ });
77
+
78
+ for (const feature of features) {
79
+ const skill = getSkillForFeature(feature);
80
+ if (skill) {
81
+ const subtype = feature === 'comments' ? commentType : feature === 'crdt' ? crdtEditorType : null;
82
+ refs.push({
83
+ feature: subtype ? `${feature} (${subtype})` : feature,
84
+ skillName: skill,
85
+ });
86
+ } else {
87
+ const subtype = feature === 'comments' ? commentType : feature === 'crdt' ? crdtEditorType : null;
88
+ refs.push({
89
+ feature: subtype ? `${feature} (${subtype})` : feature,
90
+ docsUrl: getDocMarkdownUrl(feature, subtype),
91
+ });
92
+ }
93
+ }
94
+
95
+ return refs;
96
+ }
97
+
98
+ /**
99
+ * Fetches implementation details for a comment type
100
+ *
101
+ * Strategy:
102
+ * 1. Fetch from docs.velt.dev markdown URLs directly (primary)
103
+ * 2. If that fails, use Velt Docs MCP (fallback)
104
+ * 3. If both fail, return basic structure with doc URL reference
105
+ *
106
+ * @param {Object} options - Fetch options
107
+ * @param {string} options.commentType - Comment type (freestyle, popover, etc.)
108
+ * @param {Object} [options.mcpClient] - Optional MCP client for fallback
109
+ * @returns {Promise<Object>} Implementation details
110
+ */
111
+ export async function fetchCommentImplementation(options) {
112
+ const { commentType, mcpClient } = options;
113
+
114
+ console.error(`🔍 Fetching ${commentType} implementation from .md URL...`);
115
+
116
+ // Step 1: Fetch from markdown URL (primary)
117
+ console.error(' 📄 Fetching from docs.velt.dev markdown...');
118
+ try {
119
+ const urlResult = await fetchFromMarkdownUrl('comments', commentType);
120
+ console.error(' ✅ Got documentation from markdown URL');
121
+ return urlResult;
122
+ } catch (error) {
123
+ console.error(` ⚠️ Markdown fetch failed: ${error.message}`);
124
+ }
125
+
126
+ // Step 2: Fallback to Velt Docs MCP if .md URL failed
127
+ console.error(' 📚 Falling back to Velt Docs MCP...');
128
+ try {
129
+ const query = `How do I implement ${commentType} comments in Next.js?`;
130
+ const mcpResult = await queryVeltDocsMCP(query);
131
+
132
+ if (mcpResult.success) {
133
+ console.error(' ✅ Got implementation from Velt Docs MCP');
134
+ return {
135
+ success: true,
136
+ source: 'velt-docs-mcp',
137
+ docUrl: getDocUrl('comments', commentType),
138
+ mdUrl: getDocMarkdownUrl('comments', commentType),
139
+ data: mcpResult.data,
140
+ };
141
+ }
142
+ } catch (error) {
143
+ console.error(` ⚠️ Velt Docs MCP failed: ${error.message}`);
144
+ }
145
+
146
+ // Step 3: Return basic structure with doc URL
147
+ console.error(' 📖 Returning basic structure with doc URL reference');
148
+ return getBasicStructure('comments', commentType);
149
+ }
150
+
151
+ /**
152
+ * Fetches documentation from markdown URL
153
+ */
154
+ async function fetchFromMarkdownUrl(feature, subtype = null) {
155
+ const docUrl = getDocUrl(feature, subtype);
156
+ const mdUrl = getDocMarkdownUrl(feature, subtype);
157
+
158
+ try {
159
+ console.error(` Fetching from: ${mdUrl}`);
160
+ const response = await fetch(mdUrl);
161
+
162
+ if (!response.ok) {
163
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
164
+ }
165
+
166
+ const markdown = await response.text();
167
+
168
+ return {
169
+ success: true,
170
+ source: 'docs-md-url',
171
+ docUrl,
172
+ mdUrl,
173
+ data: {
174
+ markdown,
175
+ docUrl,
176
+ },
177
+ };
178
+ } catch (error) {
179
+ throw new Error(`Failed to fetch from ${mdUrl}: ${error.message}`);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Returns a basic structure with doc URL reference
185
+ */
186
+ function getBasicStructure(feature, subtype = null) {
187
+ const docUrl = getDocUrl(feature, subtype);
188
+ const mdUrl = getDocMarkdownUrl(feature, subtype);
189
+
190
+ return {
191
+ success: true,
192
+ source: 'basic-structure',
193
+ docUrl,
194
+ mdUrl,
195
+ data: {
196
+ docUrl,
197
+ mdUrl,
198
+ note: 'Please refer to the documentation URL for implementation details',
199
+ },
200
+ warning: 'Could not fetch documentation - providing URL references only',
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Fetches implementation details for any Velt feature
206
+ *
207
+ * Strategy:
208
+ * 1. Fetch from docs.velt.dev markdown URLs directly (primary)
209
+ * 2. If that fails, use Velt Docs MCP (fallback)
210
+ * 3. If both fail, return basic structure with doc URL reference
211
+ *
212
+ * @param {Object} options - Fetch options
213
+ * @param {string} options.feature - Feature name (comments, presence, cursors, notifications, recorder, crdt)
214
+ * @param {string} [options.subtype] - Optional subtype (e.g., 'freestyle' for comments, 'tiptap' for crdt)
215
+ * @param {Object} [options.mcpClient] - Optional MCP client for fallback
216
+ * @returns {Promise<Object>} Implementation details
217
+ */
218
+ export async function fetchFeatureImplementation(options) {
219
+ const { feature, subtype = null, mcpClient } = options;
220
+
221
+ const featureDisplay = subtype ? `${feature}/${subtype}` : feature;
222
+ console.error(`🔍 Fetching ${featureDisplay} implementation from .md URL...`);
223
+
224
+ // Step 1: Fetch from markdown URL (primary)
225
+ console.error(' 📄 Fetching from docs.velt.dev markdown...');
226
+ try {
227
+ const urlResult = await fetchFromMarkdownUrl(feature, subtype);
228
+ console.error(' ✅ Got documentation from markdown URL');
229
+ return urlResult;
230
+ } catch (error) {
231
+ console.error(` ⚠️ Markdown fetch failed: ${error.message}`);
232
+ }
233
+
234
+ // Step 2: Fallback to Velt Docs MCP if .md URL failed
235
+ console.error(' 📚 Falling back to Velt Docs MCP...');
236
+ try {
237
+ const query = subtype
238
+ ? `How do I implement ${subtype} ${feature} in Next.js?`
239
+ : `How do I implement ${feature} in Next.js?`;
240
+ const mcpResult = await queryVeltDocsMCP(query);
241
+
242
+ if (mcpResult.success) {
243
+ console.error(' ✅ Got implementation from Velt Docs MCP');
244
+ return {
245
+ success: true,
246
+ source: 'velt-docs-mcp',
247
+ docUrl: getDocUrl(feature, subtype),
248
+ mdUrl: getDocMarkdownUrl(feature, subtype),
249
+ data: mcpResult.data,
250
+ };
251
+ }
252
+ } catch (error) {
253
+ console.error(` ⚠️ Velt Docs MCP failed: ${error.message}`);
254
+ }
255
+
256
+ // Step 3: Return basic structure with doc URL
257
+ console.error(' 📖 Returning basic structure with doc URL reference');
258
+ return getBasicStructure(feature, subtype);
259
+ }
260
+
261
+ /**
262
+ * Fetches implementation details for CRDT feature
263
+ *
264
+ * @param {Object} options - Fetch options
265
+ * @param {string} options.editorType - Editor type (tiptap, codemirror, blocknote)
266
+ * @param {Object} [options.mcpClient] - Optional MCP client for fallback
267
+ * @returns {Promise<Object>} Implementation details
268
+ */
269
+ export async function fetchCrdtImplementation(options) {
270
+ const { editorType, mcpClient } = options;
271
+
272
+ console.error(`🔍 Fetching CRDT ${editorType} implementation from .md URL...`);
273
+
274
+ return fetchFeatureImplementation({
275
+ feature: 'crdt',
276
+ subtype: editorType,
277
+ mcpClient,
278
+ });
279
+ }
280
+
281
+ export default {
282
+ fetchCommentImplementation,
283
+ fetchFeatureImplementation,
284
+ fetchCrdtImplementation,
285
+ FEATURE_SKILL_MAP,
286
+ getSkillForFeature,
287
+ getSkillReferences,
288
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Velt Documentation URLs
3
+ *
4
+ * Comprehensive mapping of Velt feature documentation URLs for fallback when
5
+ * Velt Docs MCP is unavailable.
6
+ */
7
+
8
+ /**
9
+ * All Velt feature documentation URLs
10
+ */
11
+ export const VELT_DOCS_URLS = {
12
+ // Getting Started
13
+ quickstart: 'https://docs.velt.dev/get-started/quickstart',
14
+ keyConcepts: 'https://docs.velt.dev/key-concepts/overview',
15
+
16
+ // Comments
17
+ comments: {
18
+ setup: {
19
+ freestyle: 'https://docs.velt.dev/async-collaboration/comments/setup/freestyle',
20
+ popover: 'https://docs.velt.dev/async-collaboration/comments/setup/popover',
21
+ page: 'https://docs.velt.dev/async-collaboration/comments/setup/page',
22
+ text: 'https://docs.velt.dev/async-collaboration/comments/setup/text',
23
+ inline: 'https://docs.velt.dev/async-collaboration/comments/setup/inline-comments',
24
+ // Purpose-built library integrations
25
+ tiptap: 'https://docs.velt.dev/async-collaboration/comments/setup/tiptap',
26
+ lexical: 'https://docs.velt.dev/async-collaboration/comments/setup/lexical',
27
+ slate: 'https://docs.velt.dev/async-collaboration/comments/setup/slatejs',
28
+ },
29
+ customizeBehavior: 'https://docs.velt.dev/async-collaboration/comments/customize-behavior',
30
+ },
31
+
32
+ // Presence
33
+ presence: {
34
+ setup: 'https://docs.velt.dev/realtime-collaboration/presence/setup',
35
+ customizeBehavior: 'https://docs.velt.dev/realtime-collaboration/presence/customize-behavior',
36
+ },
37
+
38
+ // Notifications
39
+ notifications: {
40
+ setup: 'https://docs.velt.dev/async-collaboration/notifications/setup',
41
+ customizeBehavior: 'https://docs.velt.dev/async-collaboration/notifications/customize-behavior',
42
+ },
43
+
44
+ // Recorder
45
+ recorder: {
46
+ setup: 'https://docs.velt.dev/async-collaboration/recorder/setup',
47
+ customizeBehavior: 'https://docs.velt.dev/async-collaboration/recorder/customize-behavior',
48
+ },
49
+
50
+ // Cursors
51
+ cursors: {
52
+ setup: 'https://docs.velt.dev/realtime-collaboration/cursors/setup',
53
+ customizeBehavior: 'https://docs.velt.dev/realtime-collaboration/cursors/customize-behavior',
54
+ },
55
+
56
+ // CRDT (Collaborative Real-Time Document editing)
57
+ crdt: {
58
+ setup: {
59
+ tiptap: 'https://docs.velt.dev/realtime-collaboration/crdt/setup/tiptap',
60
+ codemirror: 'https://docs.velt.dev/realtime-collaboration/crdt/setup/codemirror',
61
+ blocknote: 'https://docs.velt.dev/realtime-collaboration/crdt/setup/blocknote',
62
+ reactflow: 'https://docs.velt.dev/realtime-collaboration/crdt/setup/reactflow',
63
+ },
64
+ overview: 'https://docs.velt.dev/multiplayer-editing/overview',
65
+ },
66
+
67
+ };
68
+
69
+ /**
70
+ * Gets the documentation URL for a specific feature or comment type
71
+ *
72
+ * @param {string} feature - Feature name (comments, presence, cursors, notifications, recorder)
73
+ * @param {string} [subtype] - Optional subtype (for comments: freestyle, popover, page, text, inline, tiptap, lexical, slate)
74
+ * @param {string} [page='setup'] - Page type (setup, customizeBehavior)
75
+ * @returns {string} Documentation URL
76
+ */
77
+ export function getDocUrl(feature, subtype = null, page = 'setup') {
78
+ const featureConfig = VELT_DOCS_URLS[feature];
79
+
80
+ if (!featureConfig) {
81
+ console.warn(`Unknown feature: ${feature}, using quickstart`);
82
+ return VELT_DOCS_URLS.quickstart;
83
+ }
84
+
85
+ // If it's a simple string URL, return it
86
+ if (typeof featureConfig === 'string') {
87
+ return featureConfig;
88
+ }
89
+
90
+ // Handle comments with subtypes (freestyle, popover, etc.)
91
+ if (feature === 'comments' && subtype) {
92
+ if (featureConfig.setup && featureConfig.setup[subtype]) {
93
+ return featureConfig.setup[subtype];
94
+ }
95
+ }
96
+
97
+ // Handle CRDT with subtypes (tiptap, codemirror, blocknote)
98
+ if (feature === 'crdt' && subtype) {
99
+ if (featureConfig.setup && featureConfig.setup[subtype]) {
100
+ return featureConfig.setup[subtype];
101
+ }
102
+ }
103
+
104
+ // Handle page navigation (setup, customizeBehavior)
105
+ if (featureConfig[page]) {
106
+ return featureConfig[page];
107
+ }
108
+
109
+ // Fallback to setup
110
+ return featureConfig.setup || VELT_DOCS_URLS.quickstart;
111
+ }
112
+
113
+ /**
114
+ * Gets the markdown URL for a specific feature or comment type
115
+ *
116
+ * @param {string} feature - Feature name
117
+ * @param {string} [subtype] - Optional subtype (for comments)
118
+ * @param {string} [page='setup'] - Page type
119
+ * @returns {string} Markdown documentation URL
120
+ */
121
+ export function getDocMarkdownUrl(feature, subtype = null, page = 'setup') {
122
+ const baseUrl = getDocUrl(feature, subtype, page);
123
+ return `${baseUrl}.md`;
124
+ }
125
+
126
+ /**
127
+ * Gets all available comment types
128
+ *
129
+ * @returns {string[]} Array of comment type names
130
+ */
131
+ export function getAvailableCommentTypes() {
132
+ return Object.keys(VELT_DOCS_URLS.comments.setup);
133
+ }
134
+
135
+ export default {
136
+ VELT_DOCS_URLS,
137
+ getDocUrl,
138
+ getDocMarkdownUrl,
139
+ getAvailableCommentTypes,
140
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Velt Docs MCP Client
3
+ *
4
+ * Attempts to query Velt Docs MCP server through proper MCP protocol
5
+ * Falls back to direct documentation queries if MCP is unavailable
6
+ */
7
+
8
+ import https from 'https';
9
+ import { URL } from 'url';
10
+
11
+ /**
12
+ * Queries Velt Docs MCP server properly
13
+ *
14
+ * Since HTTP-based MCP servers require IDE client access,
15
+ * we try multiple strategies to get documentation patterns.
16
+ *
17
+ * @param {string} query - Query string
18
+ * @param {number} maxRetries - Maximum number of retries (default: 2)
19
+ * @returns {Promise<Object>} Patterns result
20
+ */
21
+ export async function queryVeltDocsMCP(query, maxRetries = 2) {
22
+ const veltDocsMCPUrl = 'https://docs.velt.dev/mcp';
23
+
24
+ // Strategy 1: Try MCP server with proper protocol and retry logic
25
+ let lastError = null;
26
+
27
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
28
+ try {
29
+ if (attempt > 0) {
30
+ const backoffDelay = Math.min(1000 * Math.pow(2, attempt - 1), 3000);
31
+ console.error(` Retry attempt ${attempt}/${maxRetries} after ${backoffDelay}ms...`);
32
+ await new Promise(resolve => setTimeout(resolve, backoffDelay));
33
+ }
34
+
35
+ console.error(' Attempting MCP protocol query...');
36
+
37
+ // First, get available tools with timeout
38
+ const toolsResponse = await makeMCPRequestWithTimeout(veltDocsMCPUrl, {
39
+ jsonrpc: '2.0',
40
+ method: 'tools/list',
41
+ id: 1,
42
+ }, 5000);
43
+
44
+ if (toolsResponse.error) {
45
+ throw new Error(toolsResponse.error.message);
46
+ }
47
+
48
+ const tools = toolsResponse.result?.tools || [];
49
+ console.error(` ✓ Found ${tools.length} tools`);
50
+
51
+ // Find search/query tool
52
+ const searchTool = tools.find(t =>
53
+ t.name.toLowerCase().includes('search') ||
54
+ t.name.toLowerCase().includes('query') ||
55
+ t.name.toLowerCase().includes('docs')
56
+ ) || tools[0];
57
+
58
+ if (!searchTool) {
59
+ throw new Error('No search tool available');
60
+ }
61
+
62
+ console.error(` Calling tool: ${searchTool.name}`);
63
+
64
+ // Call the tool with timeout
65
+ const toolResponse = await makeMCPRequestWithTimeout(veltDocsMCPUrl, {
66
+ jsonrpc: '2.0',
67
+ method: 'tools/call',
68
+ id: 2,
69
+ params: {
70
+ name: searchTool.name,
71
+ arguments: {
72
+ query: query,
73
+ },
74
+ },
75
+ }, 5000);
76
+
77
+ if (toolResponse.error) {
78
+ throw new Error(toolResponse.error.message);
79
+ }
80
+
81
+ return {
82
+ success: true,
83
+ data: toolResponse.result,
84
+ source: 'velt-docs-mcp',
85
+ };
86
+
87
+ } catch (error) {
88
+ lastError = error;
89
+ console.error(` MCP query failed (attempt ${attempt + 1}/${maxRetries + 1}): ${error.message}`);
90
+
91
+ // Don't retry on certain errors
92
+ if (error.message.includes('No search tool available') ||
93
+ error.message.includes('Failed to parse response')) {
94
+ break;
95
+ }
96
+ }
97
+ }
98
+
99
+ // All retries failed
100
+ throw lastError || new Error('MCP query failed after all retries');
101
+ }
102
+
103
+ /**
104
+ * Makes MCP protocol request with guaranteed timeout using Promise.race()
105
+ *
106
+ * This ensures the request will ALWAYS timeout, even if the server
107
+ * is sending data slowly or the connection hangs.
108
+ *
109
+ * @param {string} url - MCP server URL
110
+ * @param {Object} data - JSON-RPC request data
111
+ * @param {number} timeoutMs - Timeout in milliseconds (default: 5000)
112
+ * @returns {Promise<Object>} MCP response
113
+ */
114
+ function makeMCPRequestWithTimeout(url, data, timeoutMs = 5000) {
115
+ // Create a promise that rejects after timeout
116
+ const timeoutPromise = new Promise((_, reject) => {
117
+ setTimeout(() => {
118
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
119
+ }, timeoutMs);
120
+ });
121
+
122
+ // Create the actual request promise
123
+ const requestPromise = makeMCPRequest(url, data, timeoutMs);
124
+
125
+ // Race between request and timeout
126
+ return Promise.race([requestPromise, timeoutPromise]);
127
+ }
128
+
129
+ /**
130
+ * Makes MCP protocol request (internal)
131
+ *
132
+ * @param {string} url - MCP server URL
133
+ * @param {Object} data - JSON-RPC request data
134
+ * @param {number} socketTimeout - Socket-level timeout (default: 5000)
135
+ * @returns {Promise<Object>} MCP response
136
+ */
137
+ function makeMCPRequest(url, data, socketTimeout = 5000) {
138
+ return new Promise((resolve, reject) => {
139
+ const urlObj = new URL(url);
140
+ const postData = JSON.stringify(data);
141
+
142
+ const options = {
143
+ hostname: urlObj.hostname,
144
+ port: urlObj.port || 443,
145
+ path: urlObj.pathname,
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ 'Accept': 'application/json',
150
+ 'Content-Length': Buffer.byteLength(postData),
151
+ },
152
+ timeout: socketTimeout,
153
+ };
154
+
155
+ const req = https.request(options, (res) => {
156
+ let responseData = '';
157
+ let responseSize = 0;
158
+ const maxResponseSize = 10 * 1024 * 1024; // 10MB max response size
159
+
160
+ res.on('data', (chunk) => {
161
+ responseSize += chunk.length;
162
+
163
+ // Prevent memory exhaustion from huge responses
164
+ if (responseSize > maxResponseSize) {
165
+ req.destroy();
166
+ reject(new Error(`Response too large (exceeded ${maxResponseSize} bytes)`));
167
+ return;
168
+ }
169
+
170
+ responseData += chunk;
171
+ });
172
+
173
+ res.on('end', () => {
174
+ // Check for HTTP errors
175
+ if (res.statusCode !== 200) {
176
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
177
+ return;
178
+ }
179
+
180
+ try {
181
+ const parsed = JSON.parse(responseData);
182
+ resolve(parsed);
183
+ } catch (error) {
184
+ reject(new Error(`Failed to parse response: ${error.message}`));
185
+ }
186
+ });
187
+ });
188
+
189
+ req.on('error', (error) => {
190
+ reject(new Error(`Request error: ${error.message}`));
191
+ });
192
+
193
+ req.on('timeout', () => {
194
+ req.destroy();
195
+ reject(new Error('Socket timeout'));
196
+ });
197
+
198
+ req.write(postData);
199
+ req.end();
200
+ });
201
+ }
202
+