knowhub
Version:
Synchronize AI coding–agent knowledge files (rules, templates, guidelines) across your project.
151 lines • 6.27 kB
JavaScript
import { ResourceError } from "../../errors.js";
import { PluginConfigurationError, PluginError } from "../errors.js";
/**
* Plugin for handling HTTP(S) resources
* This maintains backward compatibility with existing `url` resources
*/
export class HttpPlugin {
name = "http";
version = "1.0.0";
description = "Fetch files from HTTP(S) URLs with advanced features";
schema = {
type: "object",
properties: {
url: {
type: "string",
description: "HTTP(S) URL to fetch",
},
headers: {
type: "object",
description: "Custom HTTP headers",
additionalProperties: { type: "string" },
},
timeout: {
type: "number",
description: "Request timeout in milliseconds",
minimum: 1000,
maximum: 300000,
},
method: {
type: "string",
description: "HTTP method",
enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"],
},
body: {
type: "string",
description: "Request body for POST/PUT requests",
},
},
required: ["url"],
additionalProperties: false,
};
async validate(config) {
if (!config || typeof config !== "object") {
throw new PluginConfigurationError(this.name, "config", "must be an object");
}
const httpConfig = config;
if (typeof httpConfig.url !== "string" || httpConfig.url.length === 0) {
throw new PluginConfigurationError(this.name, "url", "must be a non-empty string");
}
try {
const url = new URL(httpConfig.url);
if (!url.protocol.startsWith("http")) {
throw new PluginConfigurationError(this.name, "url", "must be an HTTP(S) URL");
}
}
catch (error) {
throw new PluginConfigurationError(this.name, "url", "must be a valid URL");
}
if (httpConfig.headers !== undefined) {
if (typeof httpConfig.headers !== "object" ||
httpConfig.headers === null ||
Array.isArray(httpConfig.headers)) {
throw new PluginConfigurationError(this.name, "headers", "must be an object");
}
const headers = httpConfig.headers;
for (const [key, value] of Object.entries(headers)) {
if (typeof value !== "string") {
throw new PluginConfigurationError(this.name, `headers.${key}`, "must be a string");
}
}
}
if (httpConfig.timeout !== undefined) {
if (typeof httpConfig.timeout !== "number" ||
httpConfig.timeout < 1000 ||
httpConfig.timeout > 300000) {
throw new PluginConfigurationError(this.name, "timeout", "must be a number between 1000 and 300000");
}
}
if (httpConfig.method !== undefined) {
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
if (typeof httpConfig.method !== "string" ||
!validMethods.includes(httpConfig.method.toUpperCase())) {
throw new PluginConfigurationError(this.name, "method", `must be one of: ${validMethods.join(", ")}`);
}
}
if (httpConfig.body !== undefined && typeof httpConfig.body !== "string") {
throw new PluginConfigurationError(this.name, "body", "must be a string");
}
}
async fetch(config, context) {
await this.validate(config);
const httpConfig = config;
const url = httpConfig.url;
const timeout = httpConfig.timeout || 30000;
const method = (httpConfig.method || "GET").toUpperCase();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const requestOptions = {
method,
headers: {
"User-Agent": "knowhub/1.0.0",
...httpConfig.headers,
},
signal: controller.signal,
};
if (httpConfig.body && ["POST", "PUT", "PATCH"].includes(method)) {
requestOptions.body = httpConfig.body;
}
const response = await fetch(url, requestOptions);
clearTimeout(timeoutId);
if (!response.ok) {
throw new ResourceError(`HTTP ${response.status} ${response.statusText}: Failed to fetch ${url}`, url);
}
const content = await response.text();
const lastModified = response.headers.get("last-modified");
const etag = response.headers.get("etag");
const contentType = response.headers.get("content-type");
return {
content,
isDirectory: false,
metadata: {
lastModified: lastModified ? new Date(lastModified) : undefined,
etag: etag || undefined,
version: etag || response.headers.get("x-version") || undefined,
contentType: contentType || undefined,
responseHeaders: Object.fromEntries(response.headers.entries()),
},
};
}
catch (error) {
if (error instanceof ResourceError) {
throw new PluginError(error.message, this.name, error);
}
let errorMessage = "Unknown error";
if (error instanceof Error) {
if (error.name === "AbortError") {
errorMessage = `Request timeout after ${timeout}ms`;
}
else {
errorMessage = error.message;
}
}
else {
errorMessage = String(error);
}
throw new PluginError(`Failed to fetch URL "${url}": ${errorMessage}`, this.name, error instanceof Error ? error : undefined);
}
}
}
//# sourceMappingURL=http.js.map