UNPKG

gaunt-sloth-assistant

Version:

[![Tests and Lint](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml) [![Integration Tests](https://github.co

185 lines 7.7 kB
/** * @module GthDevToolkit */ import { BaseToolkit, tool } from '@langchain/core/tools'; import { z } from 'zod'; import { spawn } from 'child_process'; import path from 'node:path'; import { displayInfo, displayError } from '#src/utils/consoleUtils.js'; import { stdout } from '#src/utils/systemUtils.js'; // Helper function to create a tool with dev type function createGthTool(fn, config, gthDevType) { const toolInstance = tool(fn, config); // eslint-disable-next-line @typescript-eslint/no-explicit-any toolInstance.gthDevType = gthDevType; return toolInstance; } // Schema definitions const RunTestsArgsSchema = z.object({}); const RunLintArgsSchema = z.object({}); const RunBuildArgsSchema = z.object({}); const RunSingleTestArgsSchema = z.object({ testPath: z.string().describe('Relative path to the test file to run'), }); const TEST_PATH_PLACEHOLDER = '${testPath}'; export default class GthDevToolkit extends BaseToolkit { tools; commands; constructor(commands = {}) { super(); this.commands = commands; this.tools = this.createTools(); } /** * Get tools filtered by operation type */ getFilteredTools(allowedOperations) { return this.tools.filter((tool) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const toolType = tool.gthDevType; return allowedOperations.includes(toolType); }); } /** * Validate test path to prevent security issues */ validateTestPath(testPath) { // Check for absolute paths if (path.isAbsolute(testPath)) { throw new Error('Absolute paths are not allowed for test files'); } // Check for directory traversal attempts if (testPath.includes('..') || testPath.includes('\\..\\') || testPath.includes('/../')) { throw new Error('Directory traversal attempts are not allowed'); } // Check for pipe attempts and other shell injection if (testPath.includes('|') || testPath.includes('&') || testPath.includes(';') || testPath.includes('`')) { throw new Error('Shell injection attempts are not allowed'); } // Check for null bytes if (testPath.includes('\0')) { throw new Error('Null bytes are not allowed in test path'); } // Normalize the path to remove any redundant separators const normalizedPath = path.normalize(testPath); // Double-check after normalization if (normalizedPath.includes('..')) { throw new Error('Directory traversal attempts are not allowed'); } return normalizedPath; } /** * Build the command for running a single test file */ buildSingleTestCommand(testPath) { if (this.commands.run_single_test) { if (this.commands.run_single_test.includes(TEST_PATH_PLACEHOLDER)) { // Interpolate if placeholder is available return this.commands.run_single_test.replace(TEST_PATH_PLACEHOLDER, testPath); } else { // Concatenate if no placeholder return `${this.commands.run_single_test} ${testPath}`; } } else { throw new Error('No test command configured'); } } async executeCommand(command, toolName) { displayInfo(`\n🔧 Executing ${toolName}: ${command}`); return new Promise((resolve, reject) => { const child = spawn(command, { shell: true, }); let output = ''; // Capture output if available (when stdio is not 'inherit') if (child.stdout) { child.stdout.on('data', (data) => { const chunk = data.toString(); stdout.write(chunk); output += chunk; }); } if (child.stderr) { child.stderr.on('data', (data) => { const chunk = data.toString(); stdout.write(chunk); output += chunk; }); } child.on('close', (code) => { if (code === 0) { resolve(`Executing '${command}'...\n\n` + `<COMMAND_OUTPUT>\n` + output + `</COMMAND_OUTPUT>\n` + `\n\nCommand '${command}' completed successfully`); } else { resolve(`Executing '${command}'...\n\n` + `<COMMAND_OUTPUT>\n` + output + `</COMMAND_OUTPUT>\n` + `\n\nCommand '${command}' exited with code ${code}`); } }); child.on('error', (error) => { const errorMsg = `Failed to execute command '${command}': ${error.message}`; displayError(errorMsg); reject(new Error(errorMsg)); }); }); } createTools() { const tools = []; if (this.commands.run_tests) { tools.push(createGthTool(async (_args) => { return await this.executeCommand(this.commands.run_tests, 'run_tests'); }, { name: 'run_tests', description: 'Execute the test suite for this project. Runs the configured test command and returns the output.' + `\nThe configured command is [${this.commands.run_tests}].`, schema: RunTestsArgsSchema, }, 'execute')); } if (this.commands.run_single_test) { tools.push(createGthTool(async (args) => { const validatedPath = this.validateTestPath(args.testPath); const command = this.buildSingleTestCommand(validatedPath); return await this.executeCommand(command, 'run_single_test'); }, { name: 'run_single_test', description: 'Execute a single test file. Runs the configured test command with the specified test file path. ' + 'The test path must be relative and cannot contain directory traversal attempts or shell injection. ' + `\nThe base command is [${this.commands.run_single_test}].`, schema: RunSingleTestArgsSchema, }, 'execute')); } if (this.commands.run_lint) { tools.push(createGthTool(async (_args) => { return await this.executeCommand(this.commands.run_lint, 'run_lint'); }, { name: 'run_lint', description: 'Run the linter on the project code. Executes the configured lint command and returns any linting errors or warnings.' + `\nThe configured command is [${this.commands.run_lint}].`, schema: RunLintArgsSchema, }, 'execute')); } if (this.commands.run_build) { tools.push(createGthTool(async (_args) => { return await this.executeCommand(this.commands.run_build, 'run_build'); }, { name: 'run_build', description: 'Build the project. Executes the configured build command and returns the build output.' + `\nThe configured command is [${this.commands.run_build}].`, schema: RunBuildArgsSchema, }, 'execute')); } return tools; } } //# sourceMappingURL=GthDevToolkit.js.map