@elyracode/http-tools 0.4.6

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.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @elyracode/http-tools
2
+
3
+ HTTP tools for Elyra -- API testing, live documentation fetching, and OpenAPI spec parsing.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/http-tools
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `execute_http_request` | Send HTTP requests (GET, POST, PUT, PATCH, DELETE) with headers, body, and auth. Returns status, headers, and body. |
16
+ | `read_url_content` | Fetch a URL and convert HTML to readable text. Perfect for reading live documentation. |
17
+ | `inspect_openapi` | Fetch and parse OpenAPI/Swagger specs. Returns structured endpoint summaries. |
18
+
19
+ ## Commands
20
+
21
+ - `/http` -- Interactive selector for all HTTP tools
22
+
23
+ ## Usage
24
+
25
+ ### API Testing
26
+ ```
27
+ > Test POST localhost:8000/api/login with email and password
28
+ > Send a GET to /api/users with Bearer token authentication
29
+ > Test all CRUD endpoints for the products API
30
+ ```
31
+
32
+ The agent sends the request, reads the response, and if something fails, it can check your logs and fix the code.
33
+
34
+ ### Live Documentation
35
+ ```
36
+ > Read the Laravel Reverb docs and show me how to set it up
37
+ > Fetch the latest React 19 changelog
38
+ > What's new in Tailwind CSS 4?
39
+ ```
40
+
41
+ The agent fetches live documentation, so it always has current information -- even for features released after its training cutoff.
42
+
43
+ ### OpenAPI / Swagger
44
+ ```
45
+ > Parse this OpenAPI spec and generate a PHP client with Guzzle
46
+ > Show me all user-related endpoints from the GitHub API
47
+ > Build TypeScript types from this Swagger spec
48
+ ```
49
+
50
+ The agent reads the entire API definition and generates integration code based on the actual schema.
51
+
52
+ ## Security
53
+
54
+ - Requests include a `User-Agent: elyra-http-tools` header
55
+ - 30-second timeout by default (configurable)
56
+ - Large responses are truncated at 50KB (HTTP) or 80KB (URL content)
57
+ - No credentials are stored -- auth headers are passed per-request
@@ -0,0 +1,395 @@
1
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
2
+ import { Type } from "typebox";
3
+
4
+ export default function (elyra: ExtensionAPI): void {
5
+
6
+ // ── Tool 1: execute_http_request ──
7
+ // "Intelligent Postman" -- send HTTP requests and analyze responses
8
+ elyra.registerTool({
9
+ name: "execute_http_request",
10
+ label: "Execute HTTP Request",
11
+ description:
12
+ "Send an HTTP request to any URL and return the response. " +
13
+ "Use this to test API endpoints, verify responses, debug issues. " +
14
+ "Supports GET, POST, PUT, PATCH, DELETE with headers, body, and auth. " +
15
+ "Returns status code, headers, and body. Useful for testing local APIs (localhost).",
16
+ parameters: Type.Object({
17
+ url: Type.String({ description: "The URL to request (e.g., http://localhost:8000/api/users)" }),
18
+ method: Type.Optional(
19
+ Type.Union(
20
+ [Type.Literal("GET"), Type.Literal("POST"), Type.Literal("PUT"), Type.Literal("PATCH"), Type.Literal("DELETE"), Type.Literal("HEAD"), Type.Literal("OPTIONS")],
21
+ { description: "HTTP method (default: GET)" },
22
+ ),
23
+ ),
24
+ headers: Type.Optional(
25
+ Type.Record(Type.String(), Type.String(), {
26
+ description: "Request headers (e.g., { \"Authorization\": \"Bearer token\", \"Content-Type\": \"application/json\" })",
27
+ }),
28
+ ),
29
+ body: Type.Optional(
30
+ Type.String({ description: "Request body (JSON string for POST/PUT/PATCH)" }),
31
+ ),
32
+ timeout: Type.Optional(
33
+ Type.Number({ description: "Timeout in milliseconds (default: 30000)" }),
34
+ ),
35
+ }),
36
+ execute: async (_toolCallId, params) => {
37
+ try {
38
+ const method = params.method ?? "GET";
39
+ const headers: Record<string, string> = {
40
+ "User-Agent": "elyra-http-tools",
41
+ ...params.headers,
42
+ };
43
+
44
+ // Auto-set Content-Type for body requests if not specified
45
+ if (params.body && !headers["Content-Type"] && !headers["content-type"]) {
46
+ headers["Content-Type"] = "application/json";
47
+ }
48
+
49
+ const response = await fetch(params.url, {
50
+ method,
51
+ headers,
52
+ body: params.body ?? undefined,
53
+ signal: AbortSignal.timeout(params.timeout ?? 30000),
54
+ });
55
+
56
+ const responseHeaders: Record<string, string> = {};
57
+ response.headers.forEach((value, key) => {
58
+ responseHeaders[key] = value;
59
+ });
60
+
61
+ const contentType = response.headers.get("content-type") ?? "";
62
+ let body: string;
63
+
64
+ if (contentType.includes("application/json")) {
65
+ const json = await response.json();
66
+ body = JSON.stringify(json, null, 2);
67
+ } else {
68
+ body = await response.text();
69
+ }
70
+
71
+ // Truncate large responses
72
+ const truncated = body.length > 50000
73
+ ? `${body.slice(0, 50000)}\n\n... (truncated, ${body.length} chars total)`
74
+ : body;
75
+
76
+ const lines: string[] = [
77
+ `${method} ${params.url}`,
78
+ `Status: ${response.status} ${response.statusText}`,
79
+ "",
80
+ "Response Headers:",
81
+ ...Object.entries(responseHeaders).map(([k, v]) => ` ${k}: ${v}`),
82
+ "",
83
+ "Body:",
84
+ truncated,
85
+ ];
86
+
87
+ return {
88
+ content: [{ type: "text", text: lines.join("\n") }],
89
+ details: {
90
+ status: response.status,
91
+ statusText: response.statusText,
92
+ contentType,
93
+ },
94
+ };
95
+ } catch (error) {
96
+ const msg = error instanceof Error ? error.message : String(error);
97
+ return {
98
+ content: [{ type: "text", text: `HTTP request failed: ${msg}` }],
99
+ details: {},
100
+ };
101
+ }
102
+ },
103
+ });
104
+
105
+ // ── Tool 2: read_url_content ──
106
+ // Fetch a URL and convert to readable text (strips HTML tags)
107
+ elyra.registerTool({
108
+ name: "read_url_content",
109
+ label: "Read URL Content",
110
+ description:
111
+ "Fetch a web page or documentation URL and return its text content. " +
112
+ "HTML is stripped to readable text. Use this to read live documentation, " +
113
+ "blog posts, changelogs, or any web content that may be newer than your training data. " +
114
+ "Especially useful for reading framework docs (Laravel, React, etc.) to get up-to-date information.",
115
+ parameters: Type.Object({
116
+ url: Type.String({ description: "The URL to fetch (e.g., https://laravel.com/docs/13.x/reverb)" }),
117
+ selector: Type.Optional(
118
+ Type.String({
119
+ description: "CSS-like content hint: 'main', 'article', 'body'. Used to focus on main content and skip navigation/footer (default: auto-detect)",
120
+ }),
121
+ ),
122
+ }),
123
+ execute: async (_toolCallId, params) => {
124
+ try {
125
+ const response = await fetch(params.url, {
126
+ headers: {
127
+ "User-Agent": "elyra-http-tools (documentation reader)",
128
+ Accept: "text/html, text/plain, application/json, text/markdown",
129
+ },
130
+ signal: AbortSignal.timeout(30000),
131
+ });
132
+
133
+ if (!response.ok) {
134
+ return {
135
+ content: [{ type: "text", text: `Failed to fetch ${params.url}: ${response.status} ${response.statusText}` }],
136
+ details: {},
137
+ };
138
+ }
139
+
140
+ const contentType = response.headers.get("content-type") ?? "";
141
+ let text: string;
142
+
143
+ if (contentType.includes("application/json")) {
144
+ const json = await response.json();
145
+ text = JSON.stringify(json, null, 2);
146
+ } else {
147
+ const html = await response.text();
148
+ if (contentType.includes("text/html")) {
149
+ text = htmlToText(html, params.selector);
150
+ } else {
151
+ text = html;
152
+ }
153
+ }
154
+
155
+ // Truncate
156
+ const truncated = text.length > 80000
157
+ ? `${text.slice(0, 80000)}\n\n... (truncated, ${text.length} chars total)`
158
+ : text;
159
+
160
+ return {
161
+ content: [{ type: "text", text: `# Content from ${params.url}\n\n${truncated}` }],
162
+ details: { url: params.url, length: text.length },
163
+ };
164
+ } catch (error) {
165
+ const msg = error instanceof Error ? error.message : String(error);
166
+ return {
167
+ content: [{ type: "text", text: `Failed to read ${params.url}: ${msg}` }],
168
+ details: {},
169
+ };
170
+ }
171
+ },
172
+ });
173
+
174
+ // ── Tool 3: inspect_openapi ──
175
+ // Fetch and parse an OpenAPI/Swagger spec
176
+ elyra.registerTool({
177
+ name: "inspect_openapi",
178
+ label: "Inspect OpenAPI Spec",
179
+ description:
180
+ "Fetch an OpenAPI/Swagger specification from a URL and return a structured summary " +
181
+ "of all endpoints, methods, parameters, request bodies, and response schemas. " +
182
+ "Use this when the user wants to build an API client, generate SDK code, " +
183
+ "or understand a third-party API structure.",
184
+ parameters: Type.Object({
185
+ url: Type.String({
186
+ description: "URL to the OpenAPI/Swagger JSON or YAML spec (e.g., https://petstore.swagger.io/v2/swagger.json)",
187
+ }),
188
+ filter: Type.Optional(
189
+ Type.String({
190
+ description: "Filter endpoints by path prefix (e.g., '/users', '/orders'). Omit for all endpoints.",
191
+ }),
192
+ ),
193
+ }),
194
+ execute: async (_toolCallId, params) => {
195
+ try {
196
+ const response = await fetch(params.url, {
197
+ headers: {
198
+ "User-Agent": "elyra-http-tools",
199
+ Accept: "application/json, application/yaml, text/yaml",
200
+ },
201
+ signal: AbortSignal.timeout(30000),
202
+ });
203
+
204
+ if (!response.ok) {
205
+ return {
206
+ content: [{ type: "text", text: `Failed to fetch spec: ${response.status} ${response.statusText}` }],
207
+ details: {},
208
+ };
209
+ }
210
+
211
+ const spec = await response.json() as Record<string, unknown>;
212
+ const info = spec.info as Record<string, unknown> | undefined;
213
+ const paths = spec.paths as Record<string, Record<string, unknown>> | undefined;
214
+
215
+ if (!paths) {
216
+ return {
217
+ content: [{ type: "text", text: "Invalid OpenAPI spec: no 'paths' found." }],
218
+ details: {},
219
+ };
220
+ }
221
+
222
+ const lines: string[] = [
223
+ `# OpenAPI: ${(info?.title as string) ?? "Unknown API"}`,
224
+ `Version: ${(info?.version as string) ?? "unknown"}`,
225
+ `Description: ${(info?.description as string) ?? "none"}`,
226
+ `Base URL: ${extractBaseUrl(spec)}`,
227
+ "",
228
+ ];
229
+
230
+ let endpointCount = 0;
231
+ for (const [path, methods] of Object.entries(paths)) {
232
+ if (params.filter && !path.startsWith(params.filter)) continue;
233
+
234
+ for (const [method, details] of Object.entries(methods)) {
235
+ if (method.startsWith("x-") || typeof details !== "object" || !details) continue;
236
+ const op = details as Record<string, unknown>;
237
+ endpointCount++;
238
+ const summary = (op.summary as string) ?? "";
239
+ const operationId = (op.operationId as string) ?? "";
240
+ lines.push(`## ${method.toUpperCase()} ${path}`);
241
+ if (summary) lines.push(`Summary: ${summary}`);
242
+ if (operationId) lines.push(`Operation: ${operationId}`);
243
+
244
+ // Parameters
245
+ const parameters = op.parameters as Array<Record<string, unknown>> | undefined;
246
+ if (parameters && parameters.length > 0) {
247
+ lines.push("Parameters:");
248
+ for (const param of parameters) {
249
+ const required = param.required ? " (required)" : "";
250
+ lines.push(` - ${param.name} [${param.in}]: ${param.description ?? (param.schema as Record<string, unknown>)?.type ?? "unknown"}${required}`);
251
+ }
252
+ }
253
+
254
+ // Request body
255
+ const requestBody = op.requestBody as Record<string, unknown> | undefined;
256
+ if (requestBody) {
257
+ const content = requestBody.content as Record<string, Record<string, unknown>> | undefined;
258
+ if (content) {
259
+ const jsonSchema = content["application/json"]?.schema;
260
+ if (jsonSchema) {
261
+ lines.push(`Request Body: ${JSON.stringify(jsonSchema, null, 2).slice(0, 500)}`);
262
+ }
263
+ }
264
+ }
265
+
266
+ // Response codes
267
+ const responses = op.responses as Record<string, Record<string, unknown>> | undefined;
268
+ if (responses) {
269
+ const codes = Object.keys(responses).join(", ");
270
+ lines.push(`Responses: ${codes}`);
271
+ }
272
+
273
+ lines.push("");
274
+ }
275
+ }
276
+
277
+ lines.unshift(`Endpoints: ${endpointCount}${params.filter ? ` (filtered by ${params.filter})` : ""}`);
278
+
279
+ const text = lines.join("\n");
280
+ const truncated = text.length > 80000
281
+ ? `${text.slice(0, 80000)}\n\n... (truncated)`
282
+ : text;
283
+
284
+ return {
285
+ content: [{ type: "text", text: truncated }],
286
+ details: { endpointCount, title: info?.title },
287
+ };
288
+ } catch (error) {
289
+ const msg = error instanceof Error ? error.message : String(error);
290
+ return {
291
+ content: [{ type: "text", text: `Failed to parse OpenAPI spec: ${msg}` }],
292
+ details: {},
293
+ };
294
+ }
295
+ },
296
+ });
297
+
298
+ // ── Command: /http ──
299
+ elyra.registerCommand("http", {
300
+ description: "HTTP tools: test APIs, fetch docs, parse OpenAPI specs",
301
+ handler: async (_args: string, ctx) => {
302
+ const options = [
303
+ "Test API -- send an HTTP request to test an endpoint",
304
+ "Read URL -- fetch web content or documentation",
305
+ "OpenAPI -- parse a Swagger/OpenAPI spec",
306
+ ];
307
+
308
+ const selected = await ctx.ui.select("HTTP Tools", options);
309
+ if (!selected) return;
310
+
311
+ if (selected.startsWith("Test API")) {
312
+ elyra.sendUserMessage("I want to test an API endpoint. Ask me for the URL, method, and any headers or body to send.");
313
+ } else if (selected.startsWith("Read URL")) {
314
+ elyra.sendUserMessage("I want to read content from a URL. Ask me which URL to fetch.");
315
+ } else if (selected.startsWith("OpenAPI")) {
316
+ elyra.sendUserMessage("I want to parse an OpenAPI spec. Ask me for the URL to the spec.");
317
+ }
318
+ },
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Strip HTML tags and extract readable text content.
324
+ * Simple but effective for documentation pages.
325
+ */
326
+ function htmlToText(html: string, selectorHint?: string): string {
327
+ let content = html;
328
+
329
+ // Try to extract main content area
330
+ if (selectorHint) {
331
+ const tagMatch = content.match(new RegExp(`<${selectorHint}[^>]*>([\\s\\S]*?)<\\/${selectorHint}>`, "i"));
332
+ if (tagMatch) content = tagMatch[1];
333
+ } else {
334
+ // Auto-detect: prefer <main>, then <article>, then <body>
335
+ for (const tag of ["main", "article"]) {
336
+ const match = content.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
337
+ if (match) {
338
+ content = match[1];
339
+ break;
340
+ }
341
+ }
342
+ }
343
+
344
+ // Remove script and style blocks
345
+ content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
346
+ content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
347
+ content = content.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "");
348
+ content = content.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "");
349
+ content = content.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, "");
350
+
351
+ // Convert common elements to text
352
+ content = content.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, "\n## $1\n");
353
+ content = content.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "\n$1\n");
354
+ content = content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n");
355
+ content = content.replace(/<br\s*\/?>/gi, "\n");
356
+ content = content.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, "\n```\n$1\n```\n");
357
+ content = content.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
358
+ content = content.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "$2 ($1)");
359
+ content = content.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, "**$1**");
360
+ content = content.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, "*$1*");
361
+
362
+ // Strip remaining HTML tags
363
+ content = content.replace(/<[^>]+>/g, "");
364
+
365
+ // Clean up HTML entities
366
+ content = content.replace(/&amp;/g, "&");
367
+ content = content.replace(/&lt;/g, "<");
368
+ content = content.replace(/&gt;/g, ">");
369
+ content = content.replace(/&quot;/g, '"');
370
+ content = content.replace(/&#39;/g, "'");
371
+ content = content.replace(/&nbsp;/g, " ");
372
+
373
+ // Clean up whitespace
374
+ content = content.replace(/\n{3,}/g, "\n\n");
375
+ content = content.trim();
376
+
377
+ return content;
378
+ }
379
+
380
+ function extractBaseUrl(spec: Record<string, unknown>): string {
381
+ // OpenAPI 3.x
382
+ const servers = spec.servers as Array<Record<string, unknown>> | undefined;
383
+ if (servers && servers.length > 0) {
384
+ return (servers[0].url as string) ?? "";
385
+ }
386
+ // Swagger 2.x
387
+ const host = spec.host as string | undefined;
388
+ const basePath = spec.basePath as string | undefined;
389
+ const schemes = spec.schemes as string[] | undefined;
390
+ if (host) {
391
+ const scheme = schemes?.[0] ?? "https";
392
+ return `${scheme}://${host}${basePath ?? ""}`;
393
+ }
394
+ return "";
395
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@elyracode/http-tools",
3
+ "version": "0.4.6",
4
+ "description": "Elyra extension for HTTP requests -- API testing, live docs fetching, and OpenAPI parsing",
5
+ "type": "module",
6
+ "keywords": [
7
+ "elyra-package",
8
+ "http",
9
+ "api-testing",
10
+ "openapi",
11
+ "swagger",
12
+ "fetch"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Knut W. Horne",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/kwhorne/elyra.git",
19
+ "directory": "packages/http-tools"
20
+ },
21
+ "elyra": {
22
+ "extensions": [
23
+ "./extensions/index.ts"
24
+ ]
25
+ },
26
+ "peerDependencies": {
27
+ "@elyracode/coding-agent": "*",
28
+ "typebox": "*"
29
+ },
30
+ "scripts": {
31
+ "clean": "echo 'nothing to clean'",
32
+ "build": "echo 'nothing to build'",
33
+ "check": "echo 'nothing to check'"
34
+ }
35
+ }