diff --git a/package-lock.json b/package-lock.json index 30c0c07d..aa60d1a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,20 +22,17 @@ "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "cron-parser": "^5.10.0", - "csv-generate": "^4.4.1", "csv-parse": "^7.0.2", "csv-stringify": "^6.8.3", "deepagents": "^1.13.1", "exceljs": "^4.4.0", "googleapis": "^176.0.0", - "graphql-request": "^6.1.0", "imap-simple": "^5.1.0", "ink": "^7.1.1", "ink-scroll-view": "^0.3.7", "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", "js-yaml": "^5.3.0", - "jsonpath-plus": "^10.3.0", "marked": "^18.0.10", "node-emoji": "^2.2.0", "nodemailer": "^9.0.5", @@ -144,15 +141,6 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "license": "MIT" }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "license": "MIT", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@grpc/grpc-js": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", @@ -368,30 +356,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, "node_modules/@langchain/core": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.9.tgz", @@ -3104,35 +3068,6 @@ "node": ">=18" } }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3147,12 +3082,6 @@ "node": ">= 8" } }, - "node_modules/csv-generate": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.6.1.tgz", - "integrity": "sha512-eELl9K716LSSeP2/YcCjch525JztnnERe3jEARWw2v1FN9ukUYfZTNYZ4Rq2Jj/MFKMauffOy9VCaqQTpFThDQ==", - "license": "MIT" - }, "node_modules/csv-parse": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.2.tgz", @@ -3929,29 +3858,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/graphql": { - "version": "16.14.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", - "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/graphql-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5" - }, - "peerDependencies": { - "graphql": "14 - 16" - } - }, "node_modules/gtoken": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", @@ -4503,15 +4409,6 @@ "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -4521,24 +4418,6 @@ "bignumber.js": "^9.0.0" } }, - "node_modules/jsonpath-plus": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", - "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -6538,12 +6417,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -6740,22 +6613,6 @@ "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", "license": "Apache-2.0" }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index ebd4f442..b8c9faa3 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "cli-table3": "^0.6.5", "cron-parser": "^5.10.0", "csv-parse": "^7.0.2", - "csv-generate": "^4.4.1", "csv-stringify": "^6.8.3", "deepagents": "^1.13.1", "exceljs": "^4.4.0", @@ -84,9 +83,7 @@ "ink-scroll-view": "^0.3.7", "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", - "graphql-request": "^6.1.0", "js-yaml": "^5.3.0", - "jsonpath-plus": "^10.3.0", "marked": "^18.0.10", "node-emoji": "^2.2.0", "nodemailer": "^9.0.5", diff --git a/src/tools/api.js b/src/tools/api.js deleted file mode 100644 index 84c31bb4..00000000 --- a/src/tools/api.js +++ /dev/null @@ -1,160 +0,0 @@ -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; -import { filterUrl } from "../sandbox/urlFilter.js"; -import { parseSizeString } from "./common.js"; - -const DEFAULT_TIMEOUT = 30000; -const DEFAULT_MAX_RESPONSE_SIZE = "10mb"; - -/** - * Zod schema for REST API client input. - */ -export const ApiClientSchema = z.object({ - url: z.string().url().describe("Target URL (http/https only)"), - method: z - .enum(["GET", "POST", "PUT", "DELETE", "PATCH"]) - .optional() - .describe("HTTP method (default: GET)"), - headers: z.record(z.string()).optional().describe("Custom headers"), - body: z.any().optional().describe("Request body (auto-serialized for POST/PUT/PATCH)"), - auth: z - .object({ - type: z.enum(["bearer", "basic", "apikey"]).describe("Authentication type"), - token: z.string().optional().describe("Bearer token or API key"), - username: z.string().optional().describe("Basic auth username"), - password: z.string().optional().describe("Basic auth password"), - headerName: z.string().optional().describe("API key header name (default: X-API-Key)"), - }) - .optional() - .describe("Authentication configuration"), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Request timeout in ms (default: 30000)"), - maxResponseSize: z.string().optional().describe("Max response body size (default: 10mb)"), -}); - -/** - * Build auth headers from auth config. - * @param {object} auth - Authentication config - * @returns {object} Headers object with auth - */ -function buildAuthHeaders(auth) { - const headers = {}; - - if (auth.type === "bearer" && auth.token) { - headers["Authorization"] = `Bearer ${auth.token}`; - } else if (auth.type === "basic" && auth.username && auth.password) { - const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64"); - headers["Authorization"] = `Basic ${encoded}`; - } else if (auth.type === "apikey" && auth.token) { - const headerName = auth.headerName || "X-API-Key"; - headers[headerName] = auth.token; - } - - return headers; -} - -/** - * Execute a REST API request with full security controls. - * @param {z.infer} input - Tool input - * @returns {Promise} Response object - */ -export async function apiClient(input) { - const { - url, - method = "GET", - headers: customHeaders = {}, - body, - auth, - timeout, - maxResponseSize, - } = input; - - // Validate URL - const urlValidation = filterUrl(url); - if (!urlValidation.allowed) { - return { ok: false, error: urlValidation.reason }; - } - - // Build request config - const timeoutMs = timeout || DEFAULT_TIMEOUT; - const maxBytes = parseSizeString(maxResponseSize || DEFAULT_MAX_RESPONSE_SIZE); - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - const requestHeaders = { ...customHeaders }; - - // Add auth headers - if (auth) { - const authHeaders = buildAuthHeaders(auth); - Object.assign(requestHeaders, authHeaders); - } - - // Auto-set content type for methods with body - if (body && ["POST", "PUT", "PATCH"].includes(method)) { - if (typeof body === "object" && !Buffer.isBuffer(body)) { - requestHeaders["Content-Type"] = "application/json"; - } - } - - const requestOptions = { - method, - headers: requestHeaders, - signal: controller.signal, - }; - - if (body && ["POST", "PUT", "PATCH"].includes(method)) { - requestOptions.body = typeof body === "string" ? body : JSON.stringify(body); - } - - try { - const response = await fetch(url, requestOptions); - clearTimeout(timeoutId); - - // Read body with size limit - const responseText = await response.text(); - - if (responseText.length > maxBytes) { - return { - ok: false, - error: `Response size (${responseText.length} bytes) exceeds limit (${maxBytes} bytes)`, - status: response.status, - statusText: response.statusText, - }; - } - - // Try to parse JSON response - let parsedBody; - try { - parsedBody = JSON.parse(responseText); - } catch (_err) { - parsedBody = responseText; - } - - return { - ok: response.ok, - status: response.status, - statusText: response.statusText, - body: parsedBody, - headers: Object.fromEntries(response.headers.entries()), - }; - } catch (err) { - clearTimeout(timeoutId); - const reason = - err.name === "AbortError" ? `Request timed out after ${timeoutMs}ms` : err.message; - return { ok: false, error: reason }; - } -} - -/** - * REST API client tool — make authenticated HTTP requests. - */ -export const apiClientTool = tool(apiClient, { - name: "apiClient", - description: - "Make authenticated REST API requests (GET, POST, PUT, DELETE, PATCH) with URL validation, timeout, and response size limits.", - schema: ApiClientSchema, -}); diff --git a/src/tools/data.js b/src/tools/data.js deleted file mode 100644 index ffa43c59..00000000 --- a/src/tools/data.js +++ /dev/null @@ -1,599 +0,0 @@ -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; -import { load as loadYaml, dump as dumpYaml } from "js-yaml"; -import { JSONPath } from "jsonpath-plus"; -import { parse } from "csv-parse/sync"; -import { stringify as stringifyCsv } from "csv-stringify/sync"; - -/** - * Zod schema for data manipulation and transformation input. - */ -export const DataToolSchema = z.object({ - action: z - .enum([ - "parse", - "serialize", - "filter", - "transform", - "validate", - "yamlParse", - "yamlSerialize", - "yamlPath", - "yamlTransform", - "yamlValidate", - "jsonToCsv", - "csvToJson", - "jsonToYaml", - "yamlToJson", - "mapFields", - ]) - .describe("Data action to perform"), - input: z.string().optional().describe("String input (JSON/YAML/CSV)"), - data: z.any().optional().describe("Object input (for serialization)"), - path: z.string().optional().describe("JSONPath or dot-notation path"), - schema: z.any().optional().describe("Schema for validation"), - mapping: z - .object({ - renames: z.record(z.string()).optional().describe("Field rename map"), - additions: z.record(z.any()).optional().describe("Fields to add"), - removals: z.array(z.string()).optional().describe("Fields to remove"), - }) - .optional() - .describe("Transformation mapping rules"), - indent: z.number().optional().describe("Serialization indent (default: 2)"), -}); - -/** - * Parse JSON string to object. - * @param {string} input - JSON string - * @returns {object} Parse result - */ -function parseJson(input) { - if (!input || typeof input !== "string") { - return { ok: false, error: "Input must be a non-empty string" }; - } - try { - const parsed = JSON.parse(input); - return { ok: true, data: parsed }; - } catch (err) { - return { ok: false, error: `JSON parse error: ${err.message}` }; - } -} - -/** - * Serialize object to JSON string. - * @param {object} data - Object to serialize - * @param {number} [indent=2] - Indentation - * @returns {object} Serialization result - */ -function serializeJson(data, indent = 2) { - try { - const serialized = JSON.stringify(data, null, indent); - return { ok: true, data: serialized }; - } catch (err) { - return { ok: false, error: `Serialization error: ${err.message}` }; - } -} - -/** - * Filter JSON using JSONPath. - * @param {object} data - Parsed JSON data - * @param {string} path - JSONPath expression - * @returns {object} Filter result - */ -function filterJson(data, path) { - if (!path) { - return { ok: false, error: "path is required for filter action" }; - } - try { - const results = JSONPath({ path, json: data }); - return { ok: true, data: results }; - } catch (err) { - return { ok: false, error: `JSONPath error: ${err.message}` }; - } -} - -/** - * Transform JSON using mapping rules. - * @param {object} data - Object to transform - * @param {object} mapping - Mapping rules - * @returns {object} Transformation result - */ -function transformJson(data, mapping) { - if (!data || typeof data !== "object" || Array.isArray(data)) { - return { ok: false, error: "Transform requires an object (not array) as input" }; - } - - const result = { ...data }; - - // Apply renames - if (mapping?.renames) { - for (const [from, to] of Object.entries(mapping.renames)) { - if (result[from] !== undefined) { - result[to] = result[from]; - delete result[from]; - } - } - } - - // Apply additions - if (mapping?.additions) { - Object.assign(result, mapping.additions); - } - - // Apply removals - if (mapping?.removals) { - for (const field of mapping.removals) { - delete result[field]; - } - } - - return { ok: true, data: result }; -} - -/** - * Validate JSON against a schema using zod-like validation. - * @param {object} data - Data to validate - * @param {object} schema - Schema definition - * @returns {object} Validation result - */ -function validateJson(data, schema) { - if (!schema) { - return { ok: false, error: "schema is required for validate action" }; - } - - const errors = []; - const valid = validateAgainstSchema(data, schema, errors, ""); - - return { - ok: true, - valid, - errors: valid ? [] : errors, - }; -} - -/** - * Recursively validate data against a schema. - * @param {any} data - Data to validate - * @param {object} schema - Schema definition - * @param {string[]} errors - Error accumulator - * @param {string} path - Current path - * @returns {boolean} Whether valid - */ -function validateAgainstSchema(data, schema, errors, path) { - if (schema.type === "object") { - if (typeof data !== "object" || Array.isArray(data)) { - errors.push(`${path || "root"}: expected object, got ${typeof data}`); - return false; - } - if (schema.properties) { - let valid = true; - for (const [key, propSchema] of Object.entries(schema.properties)) { - if (schema.required && schema.required.includes(key)) { - if (!(key in data)) { - errors.push(`${path ? `${path}.` : ""}${key}: required field missing`); - valid = false; - } - } - if (key in data) { - const fieldValid = validateAgainstSchema( - data[key], - propSchema, - errors, - `${path ? `${path}.` : ""}${key}`, - ); - if (!fieldValid) valid = false; - } - } - return valid; - } - return true; - } - - if (schema.type === "array") { - if (!Array.isArray(data)) { - errors.push(`${path || "root"}: expected array, got ${typeof data}`); - return false; - } - if (schema.items) { - for (let i = 0; i < data.length; i++) { - const valid = validateAgainstSchema( - data[i], - schema.items, - errors, - `${path ? `${path}.` : ""}[${i}]`, - ); - if (!valid) return false; - } - } - return true; - } - - if (schema.type === "string") { - if (typeof data !== "string") { - errors.push(`${path || "root"}: expected string, got ${typeof data}`); - return false; - } - } - - if (schema.type === "number" || schema.type === "integer") { - if (typeof data !== "number") { - errors.push(`${path || "root"}: expected ${schema.type}, got ${typeof data}`); - return false; - } - } - - if (schema.type === "boolean") { - if (typeof data !== "boolean") { - errors.push(`${path || "root"}: expected boolean, got ${typeof data}`); - return false; - } - } - - return true; -} - -/** - * Parse YAML string to object. - * @param {string} input - YAML string - * @returns {object} Parse result - */ -function parseYaml(input) { - if (!input || typeof input !== "string") { - return { ok: false, error: "Input must be a non-empty string" }; - } - try { - const parsed = loadYaml(input); - return { ok: true, data: parsed }; - } catch (err) { - return { ok: false, error: `YAML parse error: ${err.message}` }; - } -} - -/** - * Serialize object to YAML string. - * @param {object} data - Object to serialize - * @param {number} [indent=2] - Indentation - * @returns {object} Serialization result - */ -function serializeYaml(data, indent = 2) { - try { - const serialized = dumpYaml(data, { indent, lineWidth: 100 }); - return { ok: true, data: serialized }; - } catch (err) { - return { ok: false, error: `YAML serialization error: ${err.message}` }; - } -} - -/** - * Access YAML data using dot-notation path. - * @param {object} data - Parsed YAML data - * @param {string} path - Dot-notation path (e.g., "database.host") - * @returns {object} Access result - */ -function accessYamlPath(data, path) { - if (!path) { - return { ok: false, error: "path is required for path access" }; - } - const parts = path - .replace(/\[(\d+)\]/g, ".$1") - .split(".") - .filter(Boolean); - let current = data; - for (const part of parts) { - if (current === null || current === undefined) { - return { ok: true, data: null }; - } - current = current[part]; - } - return { ok: true, data: current }; -} - -/** - * Transform YAML using mapping rules. - * @param {object} data - Object to transform - * @param {object} mapping - Mapping rules - * @returns {object} Transformation result - */ -function transformYaml(data, mapping) { - if (!data || typeof data !== "object" || Array.isArray(data)) { - return { ok: false, error: "Transform requires an object (not array) as input" }; - } - - const result = { ...data }; - - if (mapping?.renames) { - for (const [from, to] of Object.entries(mapping.renames)) { - if (result[from] !== undefined) { - result[to] = result[from]; - delete result[from]; - } - } - } - if (mapping?.additions) { - Object.assign(result, mapping.additions); - } - if (mapping?.removals) { - for (const field of mapping.removals) { - delete result[field]; - } - } - - return { ok: true, data: result }; -} - -/** - * Validate YAML data structure. - * @param {object} data - Parsed YAML data - * @param {object} schema - Schema definition - * @returns {object} Validation result - */ -function validateYaml(data, schema) { - if (!schema) { - return { ok: false, error: "schema is required for validate action" }; - } - const errors = []; - const valid = validateAgainstSchema(data, schema, errors, ""); - return { - ok: true, - valid, - errors: valid ? [] : errors, - }; -} - -/** - * Convert JSON array to CSV. - * @param {object[]} data - Array of objects - * @param {object} mapping - Optional field mapping - * @returns {object} CSV result - */ -function jsonToCsv(data) { - if (!Array.isArray(data)) { - return { ok: false, error: "JSON to CSV requires an array of objects" }; - } - if (data.length === 0) { - return { ok: true, data: "" }; - } - - // Flatten nested objects - const flatten = (obj, prefix = "") => { - const result = {}; - for (const [key, value] of Object.entries(obj)) { - const fullKey = prefix ? `${prefix}.${key}` : key; - if (value && typeof value === "object" && !Array.isArray(value)) { - Object.assign(result, flatten(value, fullKey)); - } else { - result[fullKey] = value; - } - } - return result; - }; - - const flattened = data.map((row) => flatten(row)); - const headers = [...new Set(flattened.flatMap(Object.keys))]; - - const records = flattened.map((row) => { - const record = {}; - for (const header of headers) { - record[header] = row[header] !== undefined ? row[header] : ""; - } - return record; - }); - - const csv = stringifyCsv(records, { - header: true, - columns: headers.map((h) => ({ header: h, key: h })), - }); - return { ok: true, data: csv }; -} - -/** - * Convert CSV to JSON array. - * @param {string} input - CSV string - * @returns {object} JSON result - */ -function csvToJson(input) { - if (!input || typeof input !== "string") { - return { ok: false, error: "Input must be a non-empty CSV string" }; - } - try { - const records = parse(input, { - columns: true, - skip_empty_lines: true, - trim: true, - }); - return { ok: true, data: records }; - } catch (err) { - return { ok: false, error: `CSV parse error: ${err.message}` }; - } -} - -/** - * Convert JSON to YAML. - * @param {object} data - JSON data - * @param {number} [indent=2] - Indentation - * @returns {object} YAML result - */ -function jsonToYaml(data, indent = 2) { - try { - const yaml = dumpYaml(data, { indent, lineWidth: 100 }); - return { ok: true, data: yaml }; - } catch (err) { - return { ok: false, error: `JSON to YAML error: ${err.message}` }; - } -} - -/** - * Convert YAML to JSON. - * @param {string} input - YAML string - * @returns {object} JSON result - */ -function yamlToJson(input) { - if (!input || typeof input !== "string") { - return { ok: false, error: "Input must be a non-empty YAML string" }; - } - try { - const parsed = loadYaml(input); - return { ok: true, data: JSON.stringify(parsed, null, 2) }; - } catch (err) { - return { ok: false, error: `YAML to JSON error: ${err.message}` }; - } -} - -/** - * Apply field mapping to data. - * @param {object|object[]} data - Data to transform - * @param {object} mapping - Mapping rules - * @returns {object} Transformed result - */ -function applyMapping(data, mapping) { - if (!mapping) return { ok: true, data }; - - if (Array.isArray(data)) { - const results = data.map((item) => { - const result = { ...item }; - if (mapping.renames) { - for (const [from, to] of Object.entries(mapping.renames)) { - if (result[from] !== undefined) { - result[to] = result[from]; - delete result[from]; - } - } - } - if (mapping.additions) { - Object.assign(result, mapping.additions); - } - if (mapping.removals) { - for (const field of mapping.removals) { - delete result[field]; - } - } - return result; - }); - return { ok: true, data: results }; - } - - return transformJson(data, mapping); -} - -/** - * Execute data manipulation/transformation action. - * @param {z.infer} input - Tool input - * @returns {Promise} Result object - */ -export async function dataTool(input) { - const { action } = input; - - switch (action) { - // JSON actions - case "parse": { - return parseJson(input.input); - } - case "serialize": { - return serializeJson(input.data, input.indent); - } - case "filter": { - if (!input.input) return { ok: false, error: "input is required for filter action" }; - const parseResult = parseJson(input.input); - if (!parseResult.ok) return parseResult; - return filterJson(parseResult.data, input.path); - } - case "transform": { - if (!input.data) return { ok: false, error: "data is required for transform action" }; - return transformJson(input.data, input.mapping); - } - case "validate": { - if (!input.input) return { ok: false, error: "input is required for validate action" }; - if (!input.schema) return { ok: false, error: "schema is required for validate action" }; - const parseResult = parseJson(input.input); - if (!parseResult.ok) return parseResult; - return validateJson(parseResult.data, input.schema); - } - - // YAML actions - case "yamlParse": { - return parseYaml(input.input); - } - case "yamlSerialize": { - return serializeYaml(input.data, input.indent); - } - case "yamlPath": { - if (!input.input) return { ok: false, error: "input is required for yamlPath action" }; - const parseResult = parseYaml(input.input); - if (!parseResult.ok) return parseResult; - return accessYamlPath(parseResult.data, input.path); - } - case "yamlTransform": { - if (!input.input) return { ok: false, error: "input is required for yamlTransform action" }; - const parseResult = parseYaml(input.input); - if (!parseResult.ok) return parseResult; - return transformYaml(parseResult.data, input.mapping); - } - case "yamlValidate": { - if (!input.input) return { ok: false, error: "input is required for yamlValidate action" }; - if (!input.schema) return { ok: false, error: "schema is required for yamlValidate action" }; - const parseResult = parseYaml(input.input); - if (!parseResult.ok) return parseResult; - return validateYaml(parseResult.data, input.schema); - } - - // Data transformation actions - case "jsonToCsv": { - if (!input.input) return { ok: false, error: "input is required for jsonToCsv action" }; - const parseResult = parseJson(input.input); - if (!parseResult.ok) return parseResult; - return jsonToCsv(parseResult.data, input.mapping); - } - case "csvToJson": { - return csvToJson(input.input); - } - case "jsonToYaml": { - if (!input.input) return { ok: false, error: "input is required for jsonToYaml action" }; - const parseResult = parseJson(input.input); - if (!parseResult.ok) return parseResult; - return jsonToYaml(parseResult.data, input.indent); - } - case "yamlToJson": { - return yamlToJson(input.input); - } - case "mapFields": { - if (!input.input) return { ok: false, error: "input is required for mapFields action" }; - // Try JSON first, then YAML - let parsed; - let isJson = false; - try { - parsed = JSON.parse(input.input); - isJson = true; - } catch (_err) { - try { - parsed = loadYaml(input.input); - isJson = false; - } catch (_err2) { - return { ok: false, error: "Input is neither valid JSON nor YAML" }; - } - } - const result = applyMapping(parsed, input.mapping); - if (!result.ok) return result; - // Return in the same format - return isJson - ? { ok: true, data: JSON.stringify(result.data, null, 2) } - : { ok: true, data: stringify(result.data, { indent: 2, lineWidth: 100 }) }; - } - - default: - return { - ok: false, - error: `Unknown action: "${action}". Valid actions: parse, serialize, filter, transform, validate, yamlParse, yamlSerialize, yamlPath, yamlTransform, yamlValidate, jsonToCsv, csvToJson, jsonToYaml, yamlToJson, mapFields`, - }; - } -} - -/** - * Data manipulation tool — JSON, YAML, CSV parsing, filtering, transformation, and conversion. - */ -export const dataToolTool = tool(dataTool, { - name: "dataTool", - description: - "Manipulate structured data: parse/serialize JSON/YAML, JSONPath filtering, field mapping, and format conversion (JSON↔CSV, JSON↔YAML).", - schema: DataToolSchema, -}); diff --git a/src/tools/graphql.js b/src/tools/graphql.js deleted file mode 100644 index 47fa6232..00000000 --- a/src/tools/graphql.js +++ /dev/null @@ -1,206 +0,0 @@ -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; -import { filterUrl } from "../sandbox/urlFilter.js"; - -const DEFAULT_TIMEOUT = 30000; -const DEFAULT_MAX_DEPTH = 10; -const DEFAULT_MAX_COMPLEXITY = 1000; - -/** - * Zod schema for GraphQL client input. - */ -export const GraphQLClientSchema = z.object({ - url: z.string().url().describe("GraphQL endpoint URL"), - query: z.string().describe("GraphQL query or mutation string"), - variables: z.record(z.any()).optional().describe("Query variables"), - operationName: z.string().optional().describe("Operation name"), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Request timeout in ms (default: 30000)"), - headers: z.record(z.string()).optional().describe("Custom headers"), - introspection: z.boolean().optional().describe("Enable schema introspection (default: false)"), - maxDepth: z.number().int().positive().optional().describe("Max query depth (default: 10)"), - maxComplexity: z - .number() - .int() - .positive() - .optional() - .describe("Max query complexity (default: 1000)"), -}); - -/** - * Calculate query depth from AST. - * @param {string} query - GraphQL query string - * @returns {number} Maximum depth - */ -function calculateDepth(query) { - let maxDepth = 0; - let currentDepth = 0; - let inBlock = false; - - for (let i = 0; i < query.length; i++) { - const char = query[i]; - if (char === "{") { - currentDepth++; - if (currentDepth > maxDepth) maxDepth = currentDepth; - } else if (char === "}") { - currentDepth--; - } else if (char === '"') { - // Skip string literals - inBlock = !inBlock; - while (inBlock && i < query.length - 1) { - i++; - if (query[i] === '"') { - inBlock = false; - } - } - } - } - - return maxDepth; -} - -/** - * Estimate query complexity. - * @param {string} query - GraphQL query string - * @returns {number} Estimated complexity - */ -function estimateComplexity(query) { - // Count field selections (rough estimate) - const fieldMatches = query.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\s*\(/g); - const fieldCount = fieldMatches ? fieldMatches.length : 0; - - // Count nested selections - const openBraces = (query.match(/\{/g) || []).length; - const closeBraces = (query.match(/\}/g) || []).length; - const nestingLevel = Math.min(openBraces, closeBraces); - - // Each level of nesting multiplies complexity - return fieldCount * (nestingLevel + 1); -} - -/** - * Check if a query is an introspection query. - * @param {string} query - GraphQL query string - * @returns {boolean} - */ -function isIntrospectionQuery(query) { - return query.includes("__schema") || query.includes("__type"); -} - -/** - * Execute a GraphQL query or mutation. - * @param {z.infer} input - Tool input - * @returns {Promise} Response object - */ -export async function graphQLClient(input) { - const { - url, - query, - variables, - operationName, - timeout, - headers: customHeaders = {}, - introspection, - maxDepth, - maxComplexity, - } = input; - - // Validate URL - const urlValidation = filterUrl(url); - if (!urlValidation.allowed) { - return { ok: false, error: urlValidation.reason }; - } - - // Check introspection - if (!introspection && isIntrospectionQuery(query)) { - return { ok: false, error: "Introspection is disabled. Enable introspection to query schema." }; - } - - // Check depth limit - const depth = calculateDepth(query); - if (depth > (maxDepth || DEFAULT_MAX_DEPTH)) { - return { - ok: false, - error: `Query depth (${depth}) exceeds limit (${maxDepth || DEFAULT_MAX_DEPTH})`, - }; - } - - // Check complexity limit - const complexity = estimateComplexity(query); - if (complexity > (maxComplexity || DEFAULT_MAX_COMPLEXITY)) { - return { - ok: false, - error: `Query complexity (${complexity}) exceeds limit (${maxComplexity || DEFAULT_MAX_COMPLEXITY})`, - }; - } - - // Build request - const timeoutMs = timeout || DEFAULT_TIMEOUT; - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - const requestBody = { query }; - if (variables) requestBody.variables = variables; - if (operationName) requestBody.operationName = operationName; - - const requestHeaders = { - ...customHeaders, - "Content-Type": "application/json", - }; - - try { - const response = await fetch(url, { - method: "POST", - headers: requestHeaders, - body: JSON.stringify(requestBody), - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - const responseText = await response.text(); - let parsedBody; - try { - parsedBody = JSON.parse(responseText); - } catch (_err) { - return { - ok: false, - error: `Non-JSON response: ${responseText.slice(0, 500)}`, - status: response.status, - }; - } - - // Check for GraphQL errors - if (parsedBody.errors && parsedBody.errors.length > 0) { - return { - ok: false, - errors: parsedBody.errors, - data: parsedBody.data, - }; - } - - return { - ok: true, - data: parsedBody.data, - }; - } catch (err) { - clearTimeout(timeoutId); - const reason = - err.name === "AbortError" ? `Request timed out after ${timeoutMs}ms` : err.message; - return { ok: false, error: reason }; - } -} - -/** - * GraphQL client tool — execute queries and mutations. - */ -export const graphQLClientTool = tool(graphQLClient, { - name: "graphQLClient", - description: - "Execute GraphQL queries and mutations with depth/complexity limits, timeout, and optional schema introspection.", - schema: GraphQLClientSchema, -}); diff --git a/src/tools/index.js b/src/tools/index.js index a9c62830..f2712943 100644 --- a/src/tools/index.js +++ b/src/tools/index.js @@ -24,10 +24,6 @@ import { calendar } from "./calendar/index.js"; import { pdfGenerateTool } from "./pdfGenerate.js"; import { namecom } from "./namecom/index.js"; import { pptxGenerateTool } from "./fileCreate/pptx.js"; -import { apiClientTool } from "./api.js"; -import { graphQLClientTool } from "./graphql.js"; -import { webhookManagementTool } from "./webhooks.js"; -import { dataToolTool } from "./data.js"; /** * Maps tool names to required permission scopes. @@ -62,10 +58,6 @@ export const TOOL_PERMISSIONS = { pdfGenerate: ["filesystem:read", "filesystem:write", "network:outbound"], namecom: ["network:outbound"], pptxGenerate: ["filesystem:write"], - apiClientTool: ["network:outbound"], - graphQLClientTool: ["network:outbound"], - webhookManagementTool: ["network:outbound"], - dataToolTool: ["filesystem:read"], }; /** @@ -131,10 +123,6 @@ export const TOOL_CLASSIFICATIONS = { pdfGenerate: ["search", "research", "coding", "documentation", "debug"], namecom: ["search", "research", "coding", "documentation", "debug"], pptxGenerate: ["search", "research", "coding", "documentation", "debug"], - apiClientTool: ["search", "research", "coding", "debug"], - graphQLClientTool: ["search", "research", "coding", "debug"], - webhookManagementTool: ["security-audit", "coding", "debug"], - dataToolTool: ["search", "research", "coding", "documentation", "debug"], }; /** @@ -201,10 +189,6 @@ export const TOOLS = { pdfGenerate: pdfGenerateTool, namecom, pptxGenerate: pptxGenerateTool, - apiClientTool, - graphQLClientTool, - webhookManagementTool, - dataToolTool, }; /** diff --git a/src/tools/webhooks.js b/src/tools/webhooks.js deleted file mode 100644 index 3f0d8899..00000000 --- a/src/tools/webhooks.js +++ /dev/null @@ -1,303 +0,0 @@ -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; -import { createHash, randomUUID } from "node:crypto"; -import { filterUrl } from "../sandbox/urlFilter.js"; - -const DEFAULT_RATE_LIMIT = 100; -const TIMESTAMP_WINDOW_MS = 5 * 60 * 1000; // 5 minutes - -/** - * Zod schema for webhook management input. - */ -export const WebhookSchema = z.object({ - action: z - .enum(["create", "list", "delete", "verify", "deliver"]) - .describe("Webhook action to perform"), - url: z.string().url().optional().describe("Webhook URL"), - secret: z.string().optional().describe("Webhook secret for HMAC signing"), - events: z.array(z.string()).optional().describe("Events to subscribe to"), - payload: z.any().optional().describe("Webhook payload"), - webhookId: z.string().optional().describe("Webhook ID for delete/verify/deliver"), - signature: z.string().optional().describe("HMAC signature header value"), - timestamp: z.string().optional().describe("Timestamp header value (ISO 8601)"), - headers: z.record(z.string()).optional().describe("Custom headers for delivery"), - rateLimit: z - .number() - .int() - .positive() - .optional() - .describe("Max deliveries per minute (default: 100)"), -}); - -/** - * In-memory webhook store. - * @type {Map} - */ -const webhookStore = new Map(); - -/** - * Rate limiter state per webhook. - * @type {Map} - */ -const rateLimitState = new Map(); - -/** - * Mask a secret string for safe display. - * @param {string} secret - Secret to mask - * @returns {string} Masked secret - */ -function maskSecret(secret) { - if (!secret || secret.length <= 4) return "****"; - return secret.slice(0, 2) + "*".repeat(secret.length - 4) + secret.slice(-2); -} - -/** - * Verify HMAC-SHA256 signature. - * @param {string} payload - Raw payload string - * @param {string} signature - Expected signature (hex) - * @param {string} secret - Secret key - * @returns {boolean} Whether signature is valid - */ -function verifyHmacSignature(payload, signature, secret) { - if (!signature || !secret) return false; - const expected = createHash("sha256").update(payload).digest("hex"); - return signature === expected; -} - -/** - * Validate timestamp against 5-minute window. - * @param {string} timestampStr - ISO 8601 timestamp string - * @returns {boolean} Whether timestamp is valid - */ -function validateTimestamp(timestampStr) { - if (!timestampStr) return false; - try { - const timestamp = new Date(timestampStr).getTime(); - const now = Date.now(); - return Math.abs(now - timestamp) <= TIMESTAMP_WINDOW_MS; - } catch (_err) { - return false; - } -} - -/** - * Check rate limit for a webhook. - * @param {string} webhookId - Webhook ID - * @param {number} rateLimit - Max deliveries per minute - * @returns {{ allowed: boolean, retryAfter?: number }} - */ -function checkRateLimit(webhookId, rateLimit = DEFAULT_RATE_LIMIT) { - const now = Date.now(); - const windowMs = 60 * 1000; // 1 minute - - if (!rateLimitState.has(webhookId)) { - rateLimitState.set(webhookId, []); - } - - const timestamps = rateLimitState.get(webhookId); - // Remove timestamps outside the window - while (timestamps.length > 0 && now - timestamps[0] > windowMs) { - timestamps.shift(); - } - - if (timestamps.length >= rateLimit) { - const oldest = timestamps[0]; - const retryAfter = Math.ceil((oldest + windowMs - now) / 1000); - return { allowed: false, retryAfter }; - } - - timestamps.push(now); - return { allowed: true }; -} - -/** - * Deliver a webhook payload to a registered endpoint. - * @param {string} webhookId - Webhook ID - * @param {object} payload - Payload to deliver - * @param {number} rateLimit - Rate limit per minute - * @returns {Promise} Delivery result - */ -async function deliverWebhook(webhookId, payload, rateLimit = DEFAULT_RATE_LIMIT) { - const webhook = webhookStore.get(webhookId); - if (!webhook) { - return { ok: false, error: `Webhook not found: ${webhookId}` }; - } - - // Check rate limit - const rateCheck = checkRateLimit(webhookId, rateLimit); - if (!rateCheck.allowed) { - return { - ok: false, - error: `Rate limit exceeded. Retry after ${rateCheck.retryAfter}s`, - rateLimited: true, - retryAfter: rateCheck.retryAfter, - }; - } - - // Build payload with metadata - const deliveryPayload = { - ...payload, - _id: webhookId, - _events: webhook.events || ["*"], - _timestamp: new Date().toISOString(), - }; - - // Sign payload - const payloadStr = JSON.stringify(deliveryPayload); - const signature = createHash("sha256") - .update(payloadStr + webhook.secret) - .digest("hex"); - - // Build headers - const headers = { - "Content-Type": "application/json", - "X-Webhook-Signature": `sha256=${signature}`, - "X-Webhook-Timestamp": new Date().toISOString(), - "X-Webhook-Id": webhookId, - ...webhook.headers, - }; - - // Deliver - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - - try { - const response = await fetch(webhook.url, { - method: "POST", - headers, - body: payloadStr, - signal: controller.signal, - }); - clearTimeout(timeoutId); - - return { - ok: response.ok, - status: response.status, - statusText: response.statusText, - }; - } catch (err) { - clearTimeout(timeoutId); - return { - ok: false, - error: `Delivery failed: ${err.message}`, - }; - } -} - -/** - * Execute webhook management action. - * @param {z.infer} input - Tool input - * @returns {Promise} Result object - */ -export async function webhookManagement(input) { - const { action } = input; - - switch (action) { - case "create": { - const { url, secret, events, headers } = input; - if (!url) return { ok: false, error: "url is required for create action" }; - - const urlValidation = filterUrl(url); - if (!urlValidation.allowed) { - return { ok: false, error: urlValidation.reason }; - } - - const webhookId = randomUUID(); - webhookStore.set(webhookId, { - url, - secret: secret || randomUUID(), - events: events || ["*"], - headers: headers || {}, - createdAt: new Date().toISOString(), - }); - - return { - ok: true, - webhookId, - url, - secret: webhookStore.get(webhookId).secret, - events: events || ["*"], - message: "Webhook created successfully", - }; - } - - case "list": { - const webhooks = []; - for (const [id, webhook] of webhookStore) { - webhooks.push({ - webhookId: id, - url: webhook.url, - events: webhook.events, - createdAt: webhook.createdAt, - secret: maskSecret(webhook.secret), - }); - } - return { ok: true, webhooks }; - } - - case "delete": { - const { webhookId } = input; - if (!webhookId) { - return { ok: false, error: "webhookId is required for delete action" }; - } - if (!webhookStore.has(webhookId)) { - return { ok: false, error: `Webhook not found: ${webhookId}` }; - } - webhookStore.delete(webhookId); - rateLimitState.delete(webhookId); - return { ok: true, message: `Webhook ${webhookId} deleted` }; - } - - case "verify": { - const { webhookId, payload, signature, timestamp } = input; - if (!webhookId) { - return { ok: false, error: "webhookId is required for verify action" }; - } - const webhook = webhookStore.get(webhookId); - if (!webhook) { - return { ok: false, error: `Webhook not found: ${webhookId}` }; - } - - // Validate timestamp - if (timestamp && !validateTimestamp(timestamp)) { - return { ok: false, error: "Webhook timestamp expired (5-minute window)" }; - } - - // Verify signature - const payloadStr = typeof payload === "string" ? payload : JSON.stringify(payload); - const valid = verifyHmacSignature(payloadStr, signature, webhook.secret); - if (!valid) { - return { ok: false, error: "Invalid HMAC-SHA256 signature" }; - } - - return { ok: true, valid: true, message: "Webhook signature verified" }; - } - - case "deliver": { - const { webhookId, payload, rateLimit } = input; - if (!webhookId) { - return { ok: false, error: "webhookId is required for deliver action" }; - } - if (!payload) { - return { ok: false, error: "payload is required for deliver action" }; - } - return await deliverWebhook(webhookId, payload, rateLimit); - } - - default: - return { - ok: false, - error: `Unknown action: "${action}". Valid actions: create, list, delete, verify, deliver`, - }; - } -} - -/** - * Webhook management tool — create, list, delete, verify, and deliver webhooks. - */ -export const webhookManagementTool = tool(webhookManagement, { - name: "webhookManagement", - description: - "Manage webhooks: create, list, delete, verify HMAC-SHA256 signatures, and deliver payloads with rate limiting.", - schema: WebhookSchema, -}); diff --git a/tests/unit/tool_index.test.js b/tests/unit/tool_index.test.js index 62410f1b..edb21550 100644 --- a/tests/unit/tool_index.test.js +++ b/tests/unit/tool_index.test.js @@ -154,8 +154,8 @@ describe("tools - buildToolConfig", () => { }); const toolNames = tools.map((t) => t.name); // filesystem:read enables: clarify, sampling, process (exempt), compactContext, scanAgents, - // sessionSearch, date, reflectionSessions, docx, pptx, xlsx, pdf, dataToolTool - assert.strictEqual(toolNames.length, 13); + // sessionSearch, date, reflectionSessions, docx, pptx, xlsx, pdf + assert.strictEqual(toolNames.length, 12); assert.ok(toolNames.includes("clarify")); assert.ok(toolNames.includes("sampling")); assert.ok(toolNames.includes("date")); @@ -166,6 +166,5 @@ describe("tools - buildToolConfig", () => { assert.ok(toolNames.includes("pptx")); assert.ok(toolNames.includes("xlsx")); assert.ok(toolNames.includes("pdf")); - assert.ok(toolNames.includes("dataTool")); }); }); diff --git a/tests/unit/tools/apiClient.test.js b/tests/unit/tools/apiClient.test.js deleted file mode 100644 index 168f30fa..00000000 --- a/tests/unit/tools/apiClient.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { apiClientTool, apiClient, ApiClientSchema } from "../../../src/tools/api.js"; - -describe("apiClient tool - apiClient", () => { - it("rejects file:// URLs", async () => { - const result = await apiClient({ url: "file:///etc/passwd" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("rejects gopher:// URLs", async () => { - const result = await apiClient({ url: "gopher://example.com" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("rejects dict:// URLs", async () => { - const result = await apiClient({ url: "dict://example.com" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("accepts http:// URLs", async () => { - const result = await apiClient({ url: "http://example.com" }); - // Will fail to connect but should not be blocked by URL validation - assert.ok(result.status !== undefined || result.error); - }); - - it("accepts https:// URLs", async () => { - const result = await apiClient({ url: "https://example.com" }); - assert.ok(result.status !== undefined || result.error); - }); - - it("has correct schema", () => { - assert.ok(ApiClientSchema); - const shape = ApiClientSchema.shape; - assert.ok(shape.url); - assert.ok(shape.method); - assert.ok(shape.headers); - assert.ok(shape.body); - assert.ok(shape.auth); - assert.ok(shape.timeout); - }); - - it("is exported correctly", () => { - assert.ok(apiClientTool); - assert.ok(apiClient); - }); -}); diff --git a/tests/unit/tools/data.test.js b/tests/unit/tools/data.test.js deleted file mode 100644 index 8c6e55aa..00000000 --- a/tests/unit/tools/data.test.js +++ /dev/null @@ -1,270 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { dataTool, DataToolSchema } from "../../../src/tools/data.js"; - -describe("dataTool tool - dataTool", () => { - describe("JSON parsing", () => { - it("parses valid JSON", async () => { - const result = await dataTool({ action: "parse", input: '{"foo": "bar"}' }); - assert.strictEqual(result.ok, true); - assert.deepStrictEqual(result.data, { foo: "bar" }); - }); - - it("rejects invalid JSON", async () => { - const result = await dataTool({ action: "parse", input: "not json" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("rejects empty string", async () => { - const result = await dataTool({ action: "parse", input: "" }); - assert.strictEqual(result.ok, false); - }); - }); - - describe("JSON serialization", () => { - it("serializes object", async () => { - const result = await dataTool({ action: "serialize", data: { foo: "bar" } }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes('"foo"')); - }); - - it("serializes with custom indent", async () => { - const result = await dataTool({ action: "serialize", data: { a: 1 }, indent: 4 }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes(" ")); - }); - }); - - describe("JSONPath filtering", () => { - it("filters by key path", async () => { - const result = await dataTool({ - action: "filter", - input: JSON.stringify([{ name: "Alice" }, { name: "Bob" }]), - path: "$[*].name", - }); - assert.strictEqual(result.ok, true); - assert.deepStrictEqual(result.data, ["Alice", "Bob"]); - }); - - it("returns empty array for no matches", async () => { - const result = await dataTool({ - action: "filter", - input: JSON.stringify({ a: 1 }), - path: "$.nonexistent", - }); - assert.strictEqual(result.ok, true); - assert.deepStrictEqual(result.data, []); - }); - }); - - describe("JSON transformation", () => { - it("renames fields", async () => { - const result = await dataTool({ - action: "transform", - data: { name: "Alice", age: 30 }, - mapping: { renames: { name: "title" } }, - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.data.title, "Alice"); - assert.strictEqual(result.data.name, undefined); - }); - - it("adds fields", async () => { - const result = await dataTool({ - action: "transform", - data: { name: "Alice" }, - mapping: { additions: { age: 30 } }, - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.data.age, 30); - }); - - it("removes fields", async () => { - const result = await dataTool({ - action: "transform", - data: { name: "Alice", password: "secret" }, - mapping: { removals: ["password"] }, - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.data.password, undefined); - }); - }); - - describe("YAML parsing", () => { - it("parses valid YAML", async () => { - const result = await dataTool({ action: "yamlParse", input: "foo: bar\nbaz: 123" }); - assert.strictEqual(result.ok, true); - assert.deepStrictEqual(result.data, { foo: "bar", baz: 123 }); - }); - - it("rejects invalid YAML", async () => { - const result = await dataTool({ action: "yamlParse", input: "{{invalid yaml" }); - assert.strictEqual(result.ok, false); - }); - }); - - describe("YAML serialization", () => { - it("serializes object", async () => { - const result = await dataTool({ action: "yamlSerialize", data: { foo: "bar" } }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes("foo")); - }); - }); - - describe("YAML path access", () => { - it("accesses nested value", async () => { - const result = await dataTool({ - action: "yamlPath", - input: "database:\n host: localhost\n port: 5432", - path: "database.host", - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.data, "localhost"); - }); - - it("returns null for non-existent path", async () => { - const result = await dataTool({ - action: "yamlPath", - input: "{ a: 1 }", - path: "nonexistent", - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.data, undefined); - }); - }); - - describe("JSON to CSV", () => { - it("converts simple array", async () => { - const result = await dataTool({ - action: "jsonToCsv", - input: JSON.stringify([ - { name: "Alice", age: 30 }, - { name: "Bob", age: 25 }, - ]), - }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes("name")); - assert.ok(result.data.includes("Alice")); - }); - - it("handles missing fields", async () => { - const result = await dataTool({ - action: "jsonToCsv", - input: JSON.stringify([{ name: "Alice" }, { name: "Bob", age: 25 }]), - }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes("name")); - }); - }); - - describe("CSV to JSON", () => { - it("converts CSV", async () => { - const result = await dataTool({ - action: "csvToJson", - input: "name,age\nAlice,30\nBob,25", - }); - assert.strictEqual(result.ok, true); - assert.ok(Array.isArray(result.data)); - assert.strictEqual(result.data[0].name, "Alice"); - }); - - it("handles quoted fields", async () => { - const result = await dataTool({ - action: "csvToJson", - input: 'name,city\nAlice,"New York"', - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.data[0].city, "New York"); - }); - }); - - describe("JSON to YAML", () => { - it("converts JSON to YAML", async () => { - const result = await dataTool({ - action: "jsonToYaml", - input: JSON.stringify({ foo: "bar", baz: 123 }), - }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes("foo")); - }); - }); - - describe("YAML to JSON", () => { - it("converts YAML to JSON", async () => { - const result = await dataTool({ - action: "yamlToJson", - input: "foo: bar\nbaz: 123", - }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes('"foo"')); - }); - }); - - describe("Field mapping", () => { - it("maps fields in JSON", async () => { - const result = await dataTool({ - action: "mapFields", - input: JSON.stringify({ name: "Alice", password: "secret" }), - mapping: { renames: { name: "title" }, removals: ["password"] }, - }); - assert.strictEqual(result.ok, true); - assert.ok(result.data.includes('"title"')); - assert.ok(!result.data.includes('"password"')); - }); - }); - - describe("Schema validation", () => { - it("validates against schema", async () => { - const result = await dataTool({ - action: "validate", - input: JSON.stringify({ name: "Alice", age: 30 }), - schema: { - type: "object", - properties: { - name: { type: "string" }, - age: { type: "number" }, - }, - required: ["name", "age"], - }, - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.valid, true); - }); - - it("rejects invalid data", async () => { - const result = await dataTool({ - action: "validate", - input: JSON.stringify({ name: "Alice" }), - schema: { - type: "object", - properties: { - name: { type: "string" }, - age: { type: "number" }, - }, - required: ["name", "age"], - }, - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.valid, false); - }); - }); - - describe("Unknown action", () => { - it("rejects unknown action", async () => { - const result = await dataTool({ action: "unknown" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - }); - - describe("Schema", () => { - it("has correct schema", () => { - assert.ok(DataToolSchema); - const shape = DataToolSchema.shape; - assert.ok(shape.action); - assert.ok(shape.input); - assert.ok(shape.data); - }); - }); -}); diff --git a/tests/unit/tools/graphql.test.js b/tests/unit/tools/graphql.test.js deleted file mode 100644 index 1a20afdc..00000000 --- a/tests/unit/tools/graphql.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { - graphQLClientTool, - graphQLClient, - GraphQLClientSchema, -} from "../../../src/tools/graphql.js"; - -describe("graphQLClient tool - graphQLClient", () => { - it("rejects file:// URLs", async () => { - const result = await graphQLClient({ url: "file:///etc/passwd", query: "{ test }" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("rejects gopher:// URLs", async () => { - const result = await graphQLClient({ url: "gopher://example.com", query: "{ test }" }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("accepts https:// URLs", async () => { - const result = await graphQLClient({ url: "https://example.com/graphql", query: "{ test }" }); - assert.ok(result.ok === false || result.error); - }); - - it("rejects introspection when disabled", async () => { - const result = await graphQLClient({ - url: "https://example.com/graphql", - query: "{ __schema { types { name } } }", - introspection: false, - }); - // URL validation passes, but introspection check should catch it - assert.ok(result.error?.includes("Introspection is disabled") || result.ok === false); - }); - - it("has correct schema", () => { - assert.ok(GraphQLClientSchema); - const shape = GraphQLClientSchema.shape; - assert.ok(shape.url); - assert.ok(shape.query); - assert.ok(shape.variables); - assert.ok(shape.operationName); - assert.ok(shape.timeout); - assert.ok(shape.introspection); - assert.ok(shape.maxDepth); - assert.ok(shape.maxComplexity); - }); - - it("is exported correctly", () => { - assert.ok(graphQLClientTool); - assert.ok(graphQLClient); - }); -}); diff --git a/tests/unit/tools/webhooks.test.js b/tests/unit/tools/webhooks.test.js deleted file mode 100644 index d52be7d1..00000000 --- a/tests/unit/tools/webhooks.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { - webhookManagementTool, - webhookManagement, - WebhookSchema, -} from "../../../src/tools/webhooks.js"; - -describe("webhookManagement tool - webhookManagement", () => { - it("creates a webhook", async () => { - const result = await webhookManagement({ - action: "create", - url: "https://example.com/hook", - secret: "mysecret", - }); - assert.strictEqual(result.ok, true); - assert.ok(result.webhookId); - assert.strictEqual(result.url, "https://example.com/hook"); - }); - - it("lists webhooks with masked secrets", async () => { - await webhookManagement({ - action: "create", - url: "https://example.com/hook", - secret: "mysecret", - }); - const result = await webhookManagement({ action: "list" }); - assert.strictEqual(result.ok, true); - assert.ok(Array.isArray(result.webhooks)); - assert.ok(result.webhooks.length > 0); - assert.ok(result.webhooks[0].secret.includes("*")); - }); - - it("deletes a webhook", async () => { - const createResult = await webhookManagement({ - action: "create", - url: "https://example.com/hook", - secret: "mysecret", - }); - const deleteResult = await webhookManagement({ - action: "delete", - webhookId: createResult.webhookId, - }); - assert.strictEqual(deleteResult.ok, true); - }); - - it("rejects non-existent webhook deletion", async () => { - const result = await webhookManagement({ - action: "delete", - webhookId: "non-existent-id", - }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("verifies valid HMAC signature", async () => { - const createResult = await webhookManagement({ - action: "create", - url: "https://example.com/hook", - secret: "mysecret", - }); - const { createHash } = await import("node:crypto"); - const payload = JSON.stringify({ test: true }); - // The verify action hashes just the payload (not payload + secret) - const signature = createHash("sha256").update(payload).digest("hex"); - const result = await webhookManagement({ - action: "verify", - webhookId: createResult.webhookId, - payload, - signature, - }); - assert.strictEqual(result.ok, true); - assert.strictEqual(result.valid, true); - }); - - it("rejects invalid HMAC signature", async () => { - const createResult = await webhookManagement({ - action: "create", - url: "https://example.com/hook", - secret: "mysecret", - }); - const result = await webhookManagement({ - action: "verify", - webhookId: createResult.webhookId, - payload: JSON.stringify({ test: true }), - signature: "invalid-signature", - }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("rejects file:// webhook URLs", async () => { - const result = await webhookManagement({ - action: "create", - url: "file:///etc/passwd", - secret: "mysecret", - }); - assert.strictEqual(result.ok, false); - assert.ok(result.error); - }); - - it("has correct schema", () => { - assert.ok(WebhookSchema); - const shape = WebhookSchema.shape; - assert.ok(shape.action); - assert.ok(shape.url); - assert.ok(shape.secret); - assert.ok(shape.events); - assert.ok(shape.payload); - }); - - it("is exported correctly", () => { - assert.ok(webhookManagementTool); - assert.ok(webhookManagement); - }); -});