diff --git a/eslint.config.js b/eslint.config.js index 186b488..526093f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,81 +1,78 @@ // eslint.config.js -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import eslintConfigPrettier from "eslint-config-prettier"; +import js from "@eslint/js"; +import ts from "typescript-eslint"; +import prettierRecommended from "eslint-plugin-prettier/recommended"; import importPlugin from "eslint-plugin-import"; import unusedImports from "eslint-plugin-unused-imports"; +import pluginPromise from "eslint-plugin-promise"; -export default [ +export default ts.config( + js.configs.recommended, + ts.configs.recommendedTypeChecked, + prettierRecommended, + importPlugin.flatConfigs.recommended, + pluginPromise.configs["flat/recommended"], { - ignores: ["**/dist/**", "**/node_modules/**", "**/coverage/**"], - }, - ...tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, - eslintConfigPrettier, - { - files: ["src/**/*.{js,ts}", "tests/**/*.{js,ts}", "bin/**/*.{js,ts}"], - plugins: { - import: importPlugin, - "unused-imports": unusedImports, + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parserOptions: { + project: ["./tsconfig.eslint.json"], + tsconfigRootDir: import.meta.dirname, }, - languageOptions: { - ecmaVersion: 2022, - sourceType: "module", - parser: tseslint.parser, - }, - rules: { - // Basic TypeScript rules - "@typescript-eslint/no-unused-vars": [ - "warn", - { - ignoreRestSiblings: true, - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - "unused-imports/no-unused-imports": "error", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-floating-promises": "off", - - // Basic code quality rules - "no-console": "off", - "prefer-const": "warn", - "no-var": "warn", - eqeqeq: ["warn", "always"], + }, + plugins: { + "unused-imports": unusedImports, + }, + files: ["{src,test}/**/*.{js,ts}"], + rules: { + // Basic code quality rules + "no-console": "off", + "prefer-const": "warn", + "no-var": "warn", + eqeqeq: ["warn", "always"], - // Light complexity rules - complexity: ["warn", { max: 20 }], - "max-depth": ["warn", { max: 4 }], - "max-lines-per-function": ["warn", { max: 150 }], + // Light complexity rules + complexity: ["warn", { max: 20 }], + "max-depth": ["warn", { max: 4 }], + "max-lines-per-function": ["warn", { max: 150 }], - // Error prevention - "import/no-duplicates": "error", - "no-template-curly-in-string": "warn", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], - // Format and whitespace - "max-len": [ - "warn", - { - code: 120, - ignoreComments: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - }, - ], + "import/no-unresolved": "off", + "import/named": "off", + "import/extensions": [ + "error", + "ignorePackages", + { js: "always", ts: "never" }, + ], - // Import rules - "import/extensions": ["off"], - "import/no-unresolved": "off", + "no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", - // Disable specific TypeScript rules that require type information - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-return": "off", - }, - } - ), -]; + "promise/always-return": "error", + "promise/no-return-wrap": "error", + "promise/param-names": "error", + "promise/catch-or-return": "error", + "promise/no-native": "off", + "promise/no-nesting": "warn", + "promise/no-promise-in-callback": "warn", + "promise/no-callback-in-promise": "warn", + "promise/avoid-new": "off", + "promise/no-new-statics": "error", + "promise/no-return-in-finally": "warn", + "promise/valid-params": "warn", + "promise/no-multiple-resolved": "error", + }, + } +); diff --git a/package.json b/package.json index d6505d5..a542b98 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "build:ci": "tsc", "clean": "rimraf dist", "clean:all": "rimraf dist node_modules", - "lint": "eslint . --fix", + "lint": "eslint \"{src,test}/**/*.{js,ts}\" --fix", "format": "prettier --write \"src/**/*.*\"", "test": "vitest run", "test:watch": "vitest", @@ -52,6 +52,7 @@ "@anthropic-ai/sdk": "^0.36", "chalk": "^5", "dotenv": "^16", + "eslint-plugin-promise": "^7.2.1", "semver": "^7.7.1", "source-map-support": "^0.5", "uuid": "^11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fea955..2b055ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: dotenv: specifier: ^16 version: 16.4.7 + eslint-plugin-promise: + specifier: ^7.2.1 + version: 7.2.1(eslint@9.20.0) semver: specifier: ^7.7.1 version: 7.7.1 @@ -953,6 +956,12 @@ packages: eslint-config-prettier: optional: true + eslint-plugin-promise@7.2.1: + resolution: {integrity: sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint-plugin-unused-imports@4.1.4: resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} peerDependencies: @@ -3004,6 +3013,11 @@ snapshots: optionalDependencies: eslint-config-prettier: 9.1.0(eslint@9.20.0) + eslint-plugin-promise@7.2.1(eslint@9.20.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0) + eslint: 9.20.0 + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.23.0(@typescript-eslint/parser@8.23.0(eslint@9.20.0)(typescript@5.7.3))(eslint@9.20.0)(typescript@5.7.3))(eslint@9.20.0): dependencies: eslint: 9.20.0 diff --git a/prettier.config.ts b/prettier.config.ts new file mode 100644 index 0000000..c14ed9b --- /dev/null +++ b/prettier.config.ts @@ -0,0 +1,12 @@ +// prettier.config.ts, .prettierrc.ts, prettier.config.mts, or .prettierrc.mts + +import { type Config } from "prettier"; + +const config: Config = { + trailingComma: "all", + tabWidth: 2, + semi: true, + singleQuote: true, +}; + +export default config; \ No newline at end of file diff --git a/src/commands/$default.ts b/src/commands/$default.ts index eebe900..49a14ab 100644 --- a/src/commands/$default.ts +++ b/src/commands/$default.ts @@ -1,39 +1,38 @@ -import * as fs from "fs/promises"; -import { createInterface } from "readline/promises"; -import { ArgumentsCamelCase } from "yargs"; -import { toolAgent } from "../core/toolAgent.js"; -import { SharedOptions } from "../options.js"; -import { createRequire } from "module"; -import { Logger } from "../utils/logger.js"; -import { getTools } from "../tools/getTools.js"; +import * as fs from 'fs/promises'; +import { createInterface } from 'readline/promises'; +import { ArgumentsCamelCase, Argv } from 'yargs'; +import { toolAgent } from '../core/toolAgent.js'; +import { SharedOptions } from '../options.js'; +import { Logger } from '../utils/logger.js'; +import { getTools } from '../tools/getTools.js'; -import { getAnthropicApiKeyError } from "../utils/errors.js"; +import { getAnthropicApiKeyError } from '../utils/errors.js'; +import { getPackageInfo } from '../utils/versionCheck.js'; interface Options extends SharedOptions { prompt?: string; } -export const command = "* [prompt]"; -export const describe = "Execute a prompt or start interactive mode"; +export const command = '* [prompt]'; +export const describe = 'Execute a prompt or start interactive mode'; -export const builder = (yargs) => { - return yargs.positional("prompt", { - type: "string", - description: "The prompt to execute", +export const builder = (yargs: Argv) => { + return yargs.positional('prompt', { + type: 'string', + description: 'The prompt to execute', }); // Type assertion needed due to yargs typing complexity }; export const handler = async (argv: ArgumentsCamelCase) => { - const logger = new Logger({ name: "Default" }); - const require = createRequire(import.meta.url); - const packageInfo = require("../../package.json"); + const logger = new Logger({ name: 'Default' }); + const packageInfo = getPackageInfo(); logger.info(`MyCoder v${packageInfo.version} - AI-powered coding assistant`); logger.warn( - "WARNING: This tool can do anything on your command line that you ask it to.", - "It can delete files, install software, and even send data to remote servers.", - "It is a powerful tool that should be used with caution.", - "By using this tool, you agree that the authors and contributors are not responsible for any damage that may occur as a result of using this tool.", + 'WARNING: This tool can do anything on your command line that you ask it to.', + 'It can delete files, install software, and even send data to remote servers.', + 'It is a powerful tool that should be used with caution.', + 'By using this tool, you agree that the authors and contributors are not responsible for any damage that may occur as a result of using this tool.', ); try { // Early API key check @@ -47,7 +46,7 @@ export const handler = async (argv: ArgumentsCamelCase) => { // If promptFile is specified, read from file if (argv.file) { try { - prompt = await fs.readFile(argv.file, "utf-8"); + prompt = await fs.readFile(argv.file, 'utf-8'); } catch (error: any) { logger.error( `Failed to read prompt file: ${argv.file}, ${error?.message}`, @@ -67,7 +66,7 @@ export const handler = async (argv: ArgumentsCamelCase) => { logger.info( "Type your request below or 'help' for usage information. Use Ctrl+C to exit.", ); - prompt = await readline.question("\n> "); + prompt = await readline.question('\n> '); } finally { readline.close(); } @@ -78,27 +77,27 @@ export const handler = async (argv: ArgumentsCamelCase) => { if (!prompt) { logger.error( - "No prompt provided. Either specify a prompt, use --promptFile, or run in --interactive mode.", + 'No prompt provided. Either specify a prompt, use --promptFile, or run in --interactive mode.', ); process.exit(1); } // Add the standard suffix to all prompts prompt += [ - "Please ask for clarifications if required or if the tasks is confusing.", + 'Please ask for clarifications if required or if the tasks is confusing.', "If you need more context, don't be scared to create a sub-agent to investigate and generate report back, this can save a lot of time and prevent obvious mistakes.", - "Once the task is complete ask the user, via the userPrompt tool if the results are acceptable or if changes are needed or if there are additional follow on tasks.", - ].join("\n"); + 'Once the task is complete ask the user, via the userPrompt tool if the results are acceptable or if changes are needed or if there are additional follow on tasks.', + ].join('\n'); - const tools = await getTools(); + const tools = getTools(); const result = await toolAgent(prompt, tools, logger); const output = - typeof result.result === "string" + typeof result.result === 'string' ? result.result : JSON.stringify(result.result, null, 2); - logger.info("\n=== Result ===\n", output); + logger.info('\n=== Result ===\n', output); } catch (error) { - logger.error("An error occurred:", error); + logger.error('An error occurred:', error); process.exit(1); } }; diff --git a/src/commands/tools.ts b/src/commands/tools.ts index fe4f214..6ba22d6 100644 --- a/src/commands/tools.ts +++ b/src/commands/tools.ts @@ -1,14 +1,14 @@ -import type { Argv } from "yargs"; -import { getTools } from "../tools/getTools.js"; -import type { JsonSchema7Type } from "zod-to-json-schema"; +import type { Argv } from 'yargs'; +import { getTools } from '../tools/getTools.js'; +import type { JsonSchema7Type } from 'zod-to-json-schema'; -interface ListToolsOptions { +interface Options { [key: string]: unknown; } -export const describe = "List all available tools and their capabilities"; +export const describe = 'List all available tools and their capabilities'; -export const builder = (yargs: Argv) => { +export const builder = (yargs: Argv) => { return yargs; }; @@ -16,14 +16,14 @@ function formatSchema(schema: { properties?: Record; required?: string[]; }) { - let output = ""; + let output = ''; if (schema.properties) { for (const [paramName, param] of Object.entries(schema.properties)) { const required = schema.required?.includes(paramName) - ? "" - : " (optional)"; - const description = (param as any).description || ""; + ? '' + : ' (optional)'; + const description = (param as any).description || ''; output += `${paramName}${required}: ${description}\n`; if ((param as any).type) { @@ -41,20 +41,20 @@ function formatSchema(schema: { return output; } -export const handler = async () => { +export const handler = () => { try { - const tools = await getTools(); + const tools = getTools(); - console.log("Available Tools:\\n"); + console.log('Available Tools:\\n'); for (const tool of tools) { // Tool name and description console.log(`${tool.name}`); - console.log("-".repeat(tool.name.length)); + console.log('-'.repeat(tool.name.length)); console.log(`Description: ${tool.description}\\n`); // Parameters section - console.log("Parameters:"); + console.log('Parameters:'); console.log( formatSchema( tool.parameters as { @@ -65,7 +65,7 @@ export const handler = async () => { ); // Returns section - console.log("Returns:"); + console.log('Returns:'); if (tool.returns) { console.log( formatSchema( @@ -76,14 +76,14 @@ export const handler = async () => { ), ); } else { - console.log(" Type: any"); - console.log(" Description: Tool execution result or error\\n"); + console.log(' Type: any'); + console.log(' Description: Tool execution result or error\\n'); } console.log(); // Add spacing between tools } } catch (error) { - console.error("Error listing tools:", error); + console.error('Error listing tools:', error); process.exit(1); } }; diff --git a/src/core/executeToolCall.ts b/src/core/executeToolCall.ts index fa59adf..9836f7e 100644 --- a/src/core/executeToolCall.ts +++ b/src/core/executeToolCall.ts @@ -1,5 +1,5 @@ -import { Tool, ToolCall } from "./types.js"; -import { Logger } from "../utils/logger.js"; +import { Tool, ToolCall } from './types.js'; +import { Logger } from '../utils/logger.js'; const OUTPUT_LIMIT = 12 * 1024; // 10KB limit @@ -27,7 +27,7 @@ export const executeToolCall = async ( if (tool.logParameters) { tool.logParameters(toolCall.input, toolContext); } else { - logger.info("Parameters:"); + logger.info('Parameters:'); Object.entries(toolCall.input).forEach(([name, value]) => { logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`); }); @@ -40,10 +40,10 @@ export const executeToolCall = async ( if (tool.logReturns) { tool.logReturns(output, toolContext); } else { - logger.info("Results:"); - if (typeof output === "string") { + logger.info('Results:'); + if (typeof output === 'string') { logger.info(` - ${output}`); - } else if (typeof output === "object") { + } else if (typeof output === 'object') { Object.entries(output).forEach(([name, value]) => { logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`); }); @@ -51,7 +51,7 @@ export const executeToolCall = async ( } const toolOutput = - typeof output === "string" ? output : JSON.stringify(output, null, 2); + typeof output === 'string' ? output : JSON.stringify(output, null, 2); return toolOutput.length > OUTPUT_LIMIT ? `${toolOutput.slice(0, OUTPUT_LIMIT)}...(truncated)` : toolOutput; diff --git a/src/core/toolAgent.test.ts b/src/core/toolAgent.test.ts index a1fb4b9..fdfa8b4 100644 --- a/src/core/toolAgent.test.ts +++ b/src/core/toolAgent.test.ts @@ -1,46 +1,45 @@ - -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { executeToolCall } from "./executeToolCall.js"; -import { Tool } from "./types.js"; -import { toolAgent } from "./toolAgent.js"; -import { MockLogger } from "../utils/mockLogger.js"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { executeToolCall } from './executeToolCall.js'; +import { Tool } from './types.js'; +import { toolAgent } from './toolAgent.js'; +import { MockLogger } from '../utils/mockLogger.js'; const logger = new MockLogger(); // Mock configuration for testing const testConfig = { maxIterations: 50, - model: "claude-3-5-sonnet-20241022", + model: 'claude-3-5-sonnet-20241022', maxTokens: 4096, temperature: 0.7, - getSystemPrompt: async () => "Test system prompt", + getSystemPrompt: () => 'Test system prompt', }; // Mock Anthropic client response const mockResponse = { content: [ { - type: "tool_use", - name: "sequenceComplete", - id: "1", - input: { result: "Test complete" }, + type: 'tool_use', + name: 'sequenceComplete', + id: '1', + input: { result: 'Test complete' }, }, ], usage: { input_tokens: 10, output_tokens: 10 }, }; // Mock Anthropic SDK -vi.mock("@anthropic-ai/sdk", () => ({ +vi.mock('@anthropic-ai/sdk', () => ({ default: class { messages = { - create: async () => mockResponse, + create: () => mockResponse, }; }, })); -describe("toolAgent", () => { +describe('toolAgent', () => { beforeEach(() => { - process.env.ANTHROPIC_API_KEY = "test-key"; + process.env.ANTHROPIC_API_KEY = 'test-key'; }); afterEach(() => { @@ -49,114 +48,114 @@ describe("toolAgent", () => { // Mock tool for testing const mockTool: Tool = { - name: "mockTool", - description: "A mock tool for testing", + name: 'mockTool', + description: 'A mock tool for testing', parameters: { - type: "object", + type: 'object', properties: { input: { - type: "string", - description: "Test input", + type: 'string', + description: 'Test input', }, }, - required: ["input"], + required: ['input'], }, returns: { - type: "string", - description: "The processed result", + type: 'string', + description: 'The processed result', }, - execute: async ({ input }) => `Processed: ${input}`, + execute: ({ input }) => Promise.resolve( `Processed: ${input}`), }; const sequenceCompleteTool: Tool = { - name: "sequenceComplete", - description: "Completes the sequence", + name: 'sequenceComplete', + description: 'Completes the sequence', parameters: { - type: "object", + type: 'object', properties: { result: { - type: "string", - description: "The final result", + type: 'string', + description: 'The final result', }, }, - required: ["result"], + required: ['result'], }, returns: { - type: "string", - description: "The final result", + type: 'string', + description: 'The final result', }, - execute: async ({ result }) => result, + execute: ({ result }) => Promise.resolve( result), }; - it("should execute tool calls", async () => { + it('should execute tool calls', async () => { const result = await executeToolCall( { - id: "1", - name: "mockTool", - input: { input: "test" }, + id: '1', + name: 'mockTool', + input: { input: 'test' }, }, [mockTool], - logger + logger, ); - expect(result.includes("Processed: test")).toBeTruthy(); + expect(result.includes('Processed: test')).toBeTruthy(); }); - it("should handle unknown tools", async () => { + it('should handle unknown tools', async () => { await expect( executeToolCall( { - id: "1", - name: "nonexistentTool", + id: '1', + name: 'nonexistentTool', input: {}, }, [mockTool], - logger - ) + logger, + ), ).rejects.toThrow("No tool with the name 'nonexistentTool' exists."); }); - it("should handle tool execution errors", async () => { + it('should handle tool execution errors', async () => { const errorTool: Tool = { - name: "errorTool", - description: "A tool that always fails", + name: 'errorTool', + description: 'A tool that always fails', parameters: { - type: "object", + type: 'object', properties: {}, required: [], }, returns: { - type: "string", - description: "Error message", + type: 'string', + description: 'Error message', }, - execute: async () => { - throw new Error("Deliberate failure"); + execute: () => { + throw new Error('Deliberate failure'); }, }; await expect( executeToolCall( { - id: "1", - name: "errorTool", + id: '1', + name: 'errorTool', input: {}, }, [errorTool], - logger - ) - ).rejects.toThrow("Deliberate failure"); + logger, + ), + ).rejects.toThrow('Deliberate failure'); }); // New tests for async system prompt - it("should handle async system prompt", async () => { + it('should handle async system prompt', async () => { const result = await toolAgent( - "Test prompt", + 'Test prompt', [sequenceCompleteTool], logger, - testConfig + testConfig, ); - expect(result.result).toBe("Test complete"); + expect(result.result).toBe('Test complete'); expect(result.tokens.input).toBe(10); expect(result.tokens.output).toBe(10); }); diff --git a/src/core/toolAgent.ts b/src/core/toolAgent.ts index d1bb506..af573f4 100644 --- a/src/core/toolAgent.ts +++ b/src/core/toolAgent.ts @@ -1,16 +1,16 @@ -import Anthropic from "@anthropic-ai/sdk"; -import { executeToolCall } from "./executeToolCall.js"; -import { Logger } from "../utils/logger.js"; +import Anthropic from '@anthropic-ai/sdk'; +import { executeToolCall } from './executeToolCall.js'; +import { Logger } from '../utils/logger.js'; import { Tool, TextContent, ToolUseContent, ToolResultContent, Message, -} from "./types.js"; -import { execSync } from "child_process"; +} from './types.js'; +import { execSync } from 'child_process'; -import { getAnthropicApiKeyError } from "../utils/errors.js"; +import { getAnthropicApiKeyError } from '../utils/errors.js'; export interface ToolAgentResult { result: string; @@ -23,10 +23,10 @@ export interface ToolAgentResult { const CONFIG = { maxIterations: 50, - model: "claude-3-5-sonnet-20241022", + model: 'claude-3-5-sonnet-20241022', maxTokens: 4096, temperature: 0.7, - getSystemPrompt: async () => { + getSystemPrompt: () => { // Gather context with error handling const getCommandOutput = (command: string, label: string): string => { try { @@ -37,46 +37,46 @@ const CONFIG = { }; const context = { - pwd: getCommandOutput("pwd", "current directory"), - files: getCommandOutput("ls -la", "file listing"), - system: getCommandOutput("uname -a", "system information"), + pwd: getCommandOutput('pwd', 'current directory'), + files: getCommandOutput('ls -la', 'file listing'), + system: getCommandOutput('uname -a', 'system information'), datetime: new Date().toString(), }; return [ - "You are an AI agent that can use tools to accomplish tasks.", - "", - "Current Context:", + 'You are an AI agent that can use tools to accomplish tasks.', + '', + 'Current Context:', `Directory: ${context.pwd}`, - "Files:", + 'Files:', context.files, `System: ${context.system}`, `DateTime: ${context.datetime}`, - "", - "You prefer to call tools in parallel when possible because it leads to faster execution and less resource usage.", - "When done, call the sequenceComplete tool with your results to indicate that the sequence has completed.", - "", - "For coding tasks:", - "0. Try to break large tasks into smaller sub-tasks that can be completed and verified sequentially.", + '', + 'You prefer to call tools in parallel when possible because it leads to faster execution and less resource usage.', + 'When done, call the sequenceComplete tool with your results to indicate that the sequence has completed.', + '', + 'For coding tasks:', + '0. Try to break large tasks into smaller sub-tasks that can be completed and verified sequentially.', " - trying to make lots of changes in one go can make it really hard to identify when something doesn't work", - " - use sub-agents for each sub-task, leaving the main agent in a supervisory role", - " - when possible ensure the project compiles/builds and the tests pass after each sub-task", - " - give the sub-agents the guidance and context necessary be successful", - "1. First understand the context by:", - " - Reading README.md, CONTRIBUTING.md, and similar documentation", - " - Checking project configuration files (e.g., package.json)", - " - Understanding coding standards", - "2. Ensure changes:", - " - Follow project conventions", - " - Build successfully", - " - Pass all tests", - "3. Update documentation as needed", - "4. Consider adding documentation if you encountered setup/understanding challenges", - "", - "When you run into issues or unexpected results, take a step back and read the project documentation and configuration files and look at other source files in the project for examples of what works.", - "", - "Use sub-agents for parallel tasks, providing them with specific context they need rather than having them rediscover it.", - ].join("\\n"); + ' - use sub-agents for each sub-task, leaving the main agent in a supervisory role', + ' - when possible ensure the project compiles/builds and the tests pass after each sub-task', + ' - give the sub-agents the guidance and context necessary be successful', + '1. First understand the context by:', + ' - Reading README.md, CONTRIBUTING.md, and similar documentation', + ' - Checking project configuration files (e.g., package.json)', + ' - Understanding coding standards', + '2. Ensure changes:', + ' - Follow project conventions', + ' - Build successfully', + ' - Pass all tests', + '3. Update documentation as needed', + '4. Consider adding documentation if you encountered setup/understanding challenges', + '', + 'When you run into issues or unexpected results, take a step back and read the project documentation and configuration files and look at other source files in the project for examples of what works.', + '', + 'Use sub-agents for parallel tasks, providing them with specific context they need rather than having them rediscover it.', + ].join('\\n'); }, }; @@ -91,11 +91,11 @@ function processResponse(response: Anthropic.Message) { const toolCalls: ToolUseContent[] = []; for (const message of response.content) { - if (message.type === "text") { - content.push({ type: "text", text: message.text }); - } else if (message.type === "tool_use") { + if (message.type === 'text') { + content.push({ type: 'text', text: message.text }); + } else if (message.type === 'tool_use') { const toolUse: ToolUseContent = { - type: "tool_use", + type: 'tool_use', name: message.name, id: message.id, input: message.input, @@ -122,17 +122,17 @@ async function executeTools( const results = await Promise.all( toolCalls.map(async (call) => { - let toolResult = ""; + let toolResult = ''; try { toolResult = await executeToolCall(call, tools, logger); } catch (error: any) { toolResult = `Error: Exception thrown during tool execution. Type: ${error.constructor.name}, Message: ${error.message}`; } return { - type: "tool_result" as const, + type: 'tool_result' as const, tool_use_id: call.id, content: toolResult, - isComplete: call.name === "sequenceComplete", + isComplete: call.name === 'sequenceComplete', }; }), ); @@ -146,24 +146,23 @@ async function executeTools( const sequenceCompleted = results.some((r) => r.isComplete); const completionResult = results.find((r) => r.isComplete)?.content; - messages.push({ role: "user", content: toolResults }); + messages.push({ role: 'user', content: toolResults }); if (sequenceCompleted) { - logger.verbose("Sequence completed", { completionResult }); + logger.verbose('Sequence completed', { completionResult }); } return { sequenceCompleted, completionResult, toolResults }; } - export const toolAgent = async ( initialPrompt: string, tools: Tool[], logger: Logger, config = CONFIG, ): Promise => { - logger.verbose("Starting agent execution"); - logger.verbose("Initial prompt:", initialPrompt); + logger.verbose('Starting agent execution'); + logger.verbose('Initial prompt:', initialPrompt); let totalInputTokens = 0; let totalOutputTokens = 0; @@ -175,15 +174,15 @@ export const toolAgent = async ( const client = new Anthropic({ apiKey }); const messages: Message[] = [ { - role: "user", - content: [{ type: "text", text: initialPrompt }], + role: 'user', + content: [{ type: 'text', text: initialPrompt }], }, ]; - logger.debug("User message:", initialPrompt); + logger.debug('User message:', initialPrompt); // Get the system prompt once at the start - const systemPrompt = await config.getSystemPrompt(); + const systemPrompt = config.getSystemPrompt(); for (let i = 0; i < config.maxIterations; i++) { logger.verbose( @@ -204,13 +203,13 @@ export const toolAgent = async ( description: t.description, input_schema: t.parameters as Anthropic.Tool.InputSchema, })), - tool_choice: { type: "auto" }, + tool_choice: { type: 'auto' }, }); if (!response.content.length) { const result = { result: - "Agent returned empty message implying it is done its given task", + 'Agent returned empty message implying it is done its given task', tokens: { input: totalInputTokens, output: totalOutputTokens, @@ -230,13 +229,13 @@ export const toolAgent = async ( ); const { content, toolCalls } = processResponse(response); - messages.push({ role: "assistant", content }); + messages.push({ role: 'assistant', content }); // Log the assistant's message const assistantMessage = content - .filter((c) => c.type === "text") - .map((c) => (c as TextContent).text) - .join("\\n"); + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\\n'); if (assistantMessage) { logger.info(assistantMessage); } @@ -252,7 +251,7 @@ export const toolAgent = async ( const result = { result: completionResult ?? - "Sequence explicitly completed with an empty result", + 'Sequence explicitly completed with an empty result', tokens: { input: totalInputTokens, output: totalOutputTokens, @@ -266,9 +265,9 @@ export const toolAgent = async ( } } - logger.warn("Maximum iterations reached"); + logger.warn('Maximum iterations reached'); const result = { - result: "Maximum sub-agent iterations reach without successful completion", + result: 'Maximum sub-agent iterations reach without successful completion', tokens: { input: totalInputTokens, output: totalOutputTokens, diff --git a/src/core/toolContext.ts b/src/core/toolContext.ts index 6e4f9b5..5a8bc86 100644 --- a/src/core/toolContext.ts +++ b/src/core/toolContext.ts @@ -1,5 +1,5 @@ -import { Logger } from "../utils/logger.js"; -import { ToolContext } from "./types.js"; +import { Logger } from '../utils/logger.js'; +import { ToolContext } from './types.js'; export function createToolContext(logger: Logger): ToolContext { return { diff --git a/src/core/types.ts b/src/core/types.ts index ad44090..4832b6b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,5 +1,5 @@ -import { JsonSchema7Type } from "zod-to-json-schema"; -import { Logger } from "../utils/logger.js"; +import { JsonSchema7Type } from 'zod-to-json-schema'; +import { Logger } from '../utils/logger.js'; export type ToolContext = { logger: Logger; @@ -24,16 +24,16 @@ export type ToolCall = { }; export type TextContent = { - type: "text"; + type: 'text'; text: string; }; export type ToolUseContent = { - type: "tool_use"; + type: 'tool_use'; } & ToolCall; export type AssistantMessage = { - role: "assistant"; + role: 'assistant'; content: (TextContent | ToolUseContent)[]; tokenUsage?: { promptTokens: number; @@ -43,13 +43,13 @@ export type AssistantMessage = { }; export type ToolResultContent = { - type: "tool_result"; + type: 'tool_result'; tool_use_id: string; content: string; }; export type UserMessage = { - role: "user"; + role: 'user'; content: (TextContent | ToolResultContent)[]; }; diff --git a/src/index.ts b/src/index.ts index 2b62497..8069bc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,18 @@ -import * as dotenv from "dotenv"; -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -import { Logger, LogLevel } from "./utils/logger.js"; -import { createRequire } from "module"; -import { join } from "path"; -import { fileURLToPath } from "url"; -import { fileCommands } from "yargs-file-commands"; -import { checkForUpdates } from "./utils/versionCheck.js"; +import * as dotenv from 'dotenv'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { Logger, LogLevel } from './utils/logger.js'; +import { createRequire } from 'module'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { fileCommands } from 'yargs-file-commands'; +import { checkForUpdates } from './utils/versionCheck.js'; -import sourceMapSupport from "source-map-support"; +import sourceMapSupport from 'source-map-support'; -import type { PackageJson } from "type-fest"; -import { sharedOptions } from "./options.js"; -import { getTools } from "./tools/getTools.js"; +import type { PackageJson } from 'type-fest'; +import { sharedOptions } from './options.js'; +import { getTools } from './tools/getTools.js'; sourceMapSupport.install(); @@ -24,7 +24,7 @@ const nameToLogIndex = (logLevelName: string) => { const main = async () => { dotenv.config(); - const logger = new Logger({ name: "Main" }); + const logger = new Logger({ name: 'Main' }); const updateMessage = await checkForUpdates(); if (updateMessage) { @@ -34,14 +34,14 @@ const main = async () => { } // Error handling - process.on("SIGINT", () => { - logger.warn("\nGracefully shutting down..."); + process.on('SIGINT', () => { + logger.warn('\nGracefully shutting down...'); process.exit(0); }); - process.on("uncaughtException", (error) => { + process.on('uncaughtException', (error) => { logger.error( - "Fatal error:", + 'Fatal error:', error.constructor.name, error.message, error.stack, @@ -50,31 +50,31 @@ const main = async () => { }); const require = createRequire(import.meta.url); - const packageInfo = require("../package.json") as PackageJson; + const packageInfo = require('../package.json') as PackageJson; // Get the directory where commands are located const __filename = fileURLToPath(import.meta.url); - const commandsDir = join(__filename, "..", "commands"); + const commandsDir = join(__filename, '..', 'commands'); // Set up yargs with the new CLI interface await yargs(hideBin(process.argv)) .scriptName(packageInfo.name!) .version(packageInfo.version!) .options(sharedOptions) - .alias("h", "help") - .alias("V", "version") - .middleware(async (argv) => { + .alias('h', 'help') + .alias('V', 'version') + .middleware((argv) => { // Set up logger with the specified log level argv.logger = new Logger({ name: packageInfo.name!, logLevel: nameToLogIndex(argv.log), }); - argv.tools = await getTools(); + argv.tools = getTools(); }) .command( await fileCommands({ commandDirs: [commandsDir], - logLevel: "info", + logLevel: 'info', }), ) .strict() diff --git a/src/options.ts b/src/options.ts index 4e39d3d..60f774b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,4 +1,4 @@ -import { LogLevel } from "./utils/logger.js"; +import { LogLevel } from './utils/logger.js'; export type SharedOptions = { readonly log: LogLevel; @@ -8,21 +8,21 @@ export type SharedOptions = { export const sharedOptions = { log: { - type: "string", - alias: "l", - description: "Set minimum logging level", - default: "info", - choices: ["debug", "verbose", "info", "warn", "error"], + type: 'string', + alias: 'l', + description: 'Set minimum logging level', + default: 'info', + choices: ['debug', 'verbose', 'info', 'warn', 'error'], } as const, interactive: { - type: "boolean", - alias: "i", - description: "Run in interactive mode, asking for prompts", + type: 'boolean', + alias: 'i', + description: 'Run in interactive mode, asking for prompts', default: false, } as const, file: { - type: "string", - alias: "f", - description: "Read prompt from a file", + type: 'string', + alias: 'f', + description: 'Read prompt from a file', } as const, }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index fba67f4..3721fa7 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -1,8 +1,8 @@ -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; -const settingsDir = path.join(os.homedir(), ".mycoder"); +const settingsDir = path.join(os.homedir(), '.mycoder'); export const getSettingsDir = (): string => { if (!fs.existsSync(settingsDir)) { diff --git a/src/tools/getTools.test.ts b/src/tools/getTools.test.ts index aab66f1..6ef1156 100644 --- a/src/tools/getTools.test.ts +++ b/src/tools/getTools.test.ts @@ -1,31 +1,31 @@ -import { describe, it, expect } from "vitest"; -import { getTools } from "./getTools.js"; +import { describe, it, expect } from 'vitest'; +import { getTools } from './getTools.js'; -describe("getTools", () => { - it("should return a successful result with tools", async () => { - const tools = await getTools(); +describe('getTools', () => { + it('should return a successful result with tools', () => { + const tools = getTools(); expect(tools).toBeInstanceOf(Array); expect(tools.length).toBeGreaterThanOrEqual(5); // At least core tools }); - it("should include core tools", async () => { - const tools = await getTools(); + it('should include core tools', () => { + const tools = getTools(); const toolNames = tools.map((tool) => tool.name); // Check for essential tools expect(toolNames.length).greaterThan(0); }); - it("should have unique tool names", async () => { - const tools = await getTools(); + it('should have unique tool names', () => { + const tools = getTools(); const toolNames = tools.map((tool) => tool.name); const uniqueNames = new Set(toolNames); expect(toolNames).toHaveLength(uniqueNames.size); }); - it("should have valid schema for each tool", async () => { - const tools = await getTools(); + it('should have valid schema for each tool', () => { + const tools = getTools(); for (const tool of tools) { expect(tool).toEqual( @@ -38,11 +38,11 @@ describe("getTools", () => { } }); - it("should have executable functions", async () => { - const tools = await getTools(); + it('should have executable functions', () => { + const tools = getTools(); for (const tool of tools) { - expect(tool.execute).toBeTypeOf("function"); + expect(tool.execute).toBeTypeOf('function'); } }); }); diff --git a/src/tools/getTools.ts b/src/tools/getTools.ts index 32133b7..006517c 100644 --- a/src/tools/getTools.ts +++ b/src/tools/getTools.ts @@ -1,14 +1,14 @@ -import { subAgentTool } from "../tools/interaction/subAgent.js"; -import { readFileTool } from "../tools/io/readFile.js"; -import { userPromptTool } from "../tools/interaction/userPrompt.js"; -import { sequenceCompleteTool } from "../tools/system/sequenceComplete.js"; -import { fetchTool } from "../tools/io/fetch.js"; -import { Tool } from "../core/types.js"; -import { updateFileTool } from "./io/updateFile.js"; -import { shellStartTool } from "./system/shellStart.js"; -import { shellMessageTool } from "./system/shellMessage.js"; +import { subAgentTool } from '../tools/interaction/subAgent.js'; +import { readFileTool } from '../tools/io/readFile.js'; +import { userPromptTool } from '../tools/interaction/userPrompt.js'; +import { sequenceCompleteTool } from '../tools/system/sequenceComplete.js'; +import { fetchTool } from '../tools/io/fetch.js'; +import { Tool } from '../core/types.js'; +import { updateFileTool } from './io/updateFile.js'; +import { shellStartTool } from './system/shellStart.js'; +import { shellMessageTool } from './system/shellMessage.js'; -export async function getTools(): Promise { +export function getTools(): Tool[] { return [ subAgentTool, readFileTool, diff --git a/src/tools/interaction/subAgent.test.ts b/src/tools/interaction/subAgent.test.ts index 92f6ce6..f451d21 100644 --- a/src/tools/interaction/subAgent.test.ts +++ b/src/tools/interaction/subAgent.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { subAgentTool } from "./subAgent.js"; -import { MockLogger } from "../../utils/mockLogger.js"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { subAgentTool } from './subAgent.js'; +import { MockLogger } from '../../utils/mockLogger.js'; const logger = new MockLogger(); @@ -8,68 +8,68 @@ const logger = new MockLogger(); const mockResponse = { content: [ { - type: "tool_use", - name: "sequenceComplete", - id: "1", - input: { result: "Sub-agent task complete" }, + type: 'tool_use', + name: 'sequenceComplete', + id: '1', + input: { result: 'Sub-agent task complete' }, }, ], usage: { input_tokens: 10, output_tokens: 10 }, }; // Mock Anthropic SDK -vi.mock("@anthropic-ai/sdk", () => ({ +vi.mock('@anthropic-ai/sdk', () => ({ default: class { messages = { - create: async () => mockResponse, + create: () => mockResponse, }; }, })); -describe("subAgent", () => { +describe('subAgent', () => { beforeEach(() => { - process.env.ANTHROPIC_API_KEY = "test-key"; + process.env.ANTHROPIC_API_KEY = 'test-key'; }); afterEach(() => { vi.clearAllMocks(); }); - it("should create and execute a sub-agent", async () => { + it('should create and execute a sub-agent', async () => { const result = await subAgentTool.execute( { - prompt: "Test sub-agent task", - description: "A test agent for unit testing", + prompt: 'Test sub-agent task', + description: 'A test agent for unit testing', }, { logger }, ); - expect(result.toString()).toContain("Sub-agent task complete"); + expect(result.toString()).toContain('Sub-agent task complete'); }); - it("should handle errors gracefully", async () => { + it('should handle errors gracefully', async () => { // Remove API key to trigger error delete process.env.ANTHROPIC_API_KEY; await expect( subAgentTool.execute( { - prompt: "Test task", - description: "An agent that should fail", + prompt: 'Test task', + description: 'An agent that should fail', }, { logger }, ), - ).rejects.toThrow("ANTHROPIC_API_KEY environment variable is not set"); + ).rejects.toThrow('ANTHROPIC_API_KEY environment variable is not set'); }); - it("should validate description length", async () => { + it('should validate description length', async () => { const longDescription = - "This is a very long description that exceeds the maximum allowed length of 80 characters and should cause validation to fail"; + 'This is a very long description that exceeds the maximum allowed length of 80 characters and should cause validation to fail'; await expect( subAgentTool.execute( { - prompt: "Test task", + prompt: 'Test task', description: longDescription, }, { logger }, diff --git a/src/tools/interaction/subAgent.ts b/src/tools/interaction/subAgent.ts index c033e46..273e435 100644 --- a/src/tools/interaction/subAgent.ts +++ b/src/tools/interaction/subAgent.ts @@ -1,11 +1,11 @@ -import { toolAgent } from "../../core/toolAgent.js"; -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { getTools } from "../getTools.js"; +import { toolAgent } from '../../core/toolAgent.js'; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { getTools } from '../getTools.js'; const parameterSchema = z.object({ - prompt: z.string().describe("The prompt/task for the sub-agent"), + prompt: z.string().describe('The prompt/task for the sub-agent'), description: z .string() .max(80) @@ -15,7 +15,7 @@ const parameterSchema = z.object({ const returnSchema = z .string() .describe( - "The response from the sub-agent including its reasoning and tool usage", + 'The response from the sub-agent including its reasoning and tool usage', ); type Parameters = z.infer; @@ -24,33 +24,31 @@ type ReturnType = z.infer; // Sub-agent specific configuration const subAgentConfig = { maxIterations: 50, - model: "claude-3-5-sonnet-20241022", + model: 'claude-3-5-sonnet-20241022', maxTokens: 4096, temperature: 0.7, - getSystemPrompt: async () => { + getSystemPrompt: () => { return [ - "You are a focused AI sub-agent handling a specific task.", - "You have access to the same tools as the main agent but should focus only on your assigned task.", - "When complete, call the sequenceComplete tool with your results.", - "Follow any specific conventions or requirements provided in the task context.", - "Ask the main agent for clarification if critical information is missing.", - ].join("\n"); + 'You are a focused AI sub-agent handling a specific task.', + 'You have access to the same tools as the main agent but should focus only on your assigned task.', + 'When complete, call the sequenceComplete tool with your results.', + 'Follow any specific conventions or requirements provided in the task context.', + 'Ask the main agent for clarification if critical information is missing.', + ].join('\n'); }, }; export const subAgentTool: Tool = { - name: "subAgent", + name: 'subAgent', description: - "Creates a sub-agent that has access to all tools to solve a specific task", + 'Creates a sub-agent that has access to all tools to solve a specific task', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async (params, { logger }) => { // Validate parameters const { prompt } = parameterSchema.parse(params); - const tools = (await getTools()).filter( - (tool) => tool.name !== "userPrompt", - ); + const tools = getTools().filter((tool) => tool.name !== 'userPrompt'); const result = await toolAgent(prompt, tools, logger, subAgentConfig); return result.result; // Return the result string directly diff --git a/src/tools/interaction/userPrompt.ts b/src/tools/interaction/userPrompt.ts index d0bf07a..9961a25 100644 --- a/src/tools/interaction/userPrompt.ts +++ b/src/tools/interaction/userPrompt.ts @@ -1,11 +1,11 @@ -import { Tool } from "../../core/types.js"; -import * as readline from "readline"; -import chalk from "chalk"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { Tool } from '../../core/types.js'; +import * as readline from 'readline'; +import chalk from 'chalk'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; const parameterSchema = z.object({ - prompt: z.string().describe("The prompt message to display to the user"), + prompt: z.string().describe('The prompt message to display to the user'), }); const returnSchema = z.string().describe("The user's response"); @@ -14,8 +14,8 @@ type Parameters = z.infer; type ReturnType = z.infer; export const userPromptTool: Tool = { - name: "userPrompt", - description: "Prompts the user for input and returns their response", + name: 'userPrompt', + description: 'Prompts the user for input and returns their response', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ({ prompt }, { logger }) => { @@ -33,7 +33,7 @@ export const userPromptTool: Tool = { } const response = await new Promise((resolve) => { - rl.question(chalk.green(prompt + " "), (answer) => { + rl.question(chalk.green(prompt + ' '), (answer) => { resolve(answer); }); }); diff --git a/src/tools/io/fetch.ts b/src/tools/io/fetch.ts index 85f8b3b..f5a4548 100644 --- a/src/tools/io/fetch.ts +++ b/src/tools/io/fetch.ts @@ -1,23 +1,23 @@ -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; const parameterSchema = z.object({ method: z .string() .describe( - "HTTP method to use (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)", + 'HTTP method to use (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)', ), - url: z.string().describe("URL to make the request to"), + url: z.string().describe('URL to make the request to'), params: z .record(z.any()) .optional() - .describe("Optional query parameters to append to the URL"), + .describe('Optional query parameters to append to the URL'), body: z .record(z.any()) .optional() - .describe("Optional request body (for POST, PUT, PATCH requests)"), - headers: z.record(z.string()).optional().describe("Optional request headers"), + .describe('Optional request body (for POST, PUT, PATCH requests)'), + headers: z.record(z.string()).optional().describe('Optional request headers'), }); const returnSchema = z @@ -27,14 +27,14 @@ const returnSchema = z headers: z.record(z.string()), body: z.union([z.string(), z.record(z.any())]), }) - .describe("HTTP response including status, headers, and body"); + .describe('HTTP response including status, headers, and body'); type Parameters = z.infer; type ReturnType = z.infer; export const fetchTool: Tool = { - name: "fetch", - description: "Executes HTTP requests using native Node.js fetch API", + name: 'fetch', + description: 'Executes HTTP requests using native Node.js fetch API', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ( @@ -46,7 +46,7 @@ export const fetchTool: Tool = { // Add query parameters if (params) { - logger.verbose("Adding query parameters:", params); + logger.verbose('Adding query parameters:', params); Object.entries(params).forEach(([key, value]) => urlObj.searchParams.append(key, value as string), ); @@ -57,41 +57,41 @@ export const fetchTool: Tool = { method, headers: { ...(body && - !["GET", "HEAD"].includes(method) && { - "content-type": "application/json", + !['GET', 'HEAD'].includes(method) && { + 'content-type': 'application/json', }), ...headers, }, ...(body && - !["GET", "HEAD"].includes(method) && { + !['GET', 'HEAD'].includes(method) && { body: JSON.stringify(body), }), }; - logger.verbose("Request options:", options); + logger.verbose('Request options:', options); const response = await fetch(urlObj.toString(), options); logger.verbose( `Request completed with status ${response.status} ${response.statusText}`, ); - const contentType = response.headers.get("content-type"); - const responseBody = contentType?.includes("application/json") + const contentType = response.headers.get('content-type'); + const responseBody = contentType?.includes('application/json') ? await response.json() : await response.text(); - logger.verbose("Response content-type:", contentType); + logger.verbose('Response content-type:', contentType); return { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers), - body: responseBody as ReturnType["body"], + body: responseBody as ReturnType['body'], }; }, logParameters(params, { logger }) { const { method, url, params: queryParams } = params; logger.info( - `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams)}` : ""}`, + `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}`, ); }, }; diff --git a/src/tools/io/readFile.test.ts b/src/tools/io/readFile.test.ts index 066851f..0346a01 100644 --- a/src/tools/io/readFile.test.ts +++ b/src/tools/io/readFile.test.ts @@ -1,27 +1,27 @@ -import { describe, it, expect } from "vitest"; -import { readFileTool } from "./readFile.js"; -import { MockLogger } from "../../utils/mockLogger.js"; +import { describe, it, expect } from 'vitest'; +import { readFileTool } from './readFile.js'; +import { MockLogger } from '../../utils/mockLogger.js'; const logger = new MockLogger(); -describe("readFile", () => { - it("should read a file", async () => { +describe('readFile', () => { + it('should read a file', async () => { const { content } = await readFileTool.execute( - { path: "package.json", description: "test" }, + { path: 'package.json', description: 'test' }, { logger }, ); - expect(content).toContain("mycoder"); + expect(content).toContain('mycoder'); }); - it("should handle missing files", async () => { + it('should handle missing files', async () => { try { await readFileTool.execute( - { path: "nonexistent.txt", description: "test" }, + { path: 'nonexistent.txt', description: 'test' }, { logger }, ); expect(true).toBe(false); // Should not reach here } catch (error: any) { - expect(error.message).toContain("ENOENT"); + expect(error.message).toContain('ENOENT'); } }); }); diff --git a/src/tools/io/readFile.ts b/src/tools/io/readFile.ts index 6d4e3ee..41e7a51 100644 --- a/src/tools/io/readFile.ts +++ b/src/tools/io/readFile.ts @@ -1,30 +1,30 @@ -import * as fs from "fs/promises"; -import * as path from "path"; -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; const OUTPUT_LIMIT = 10 * 1024; // 10KB limit const parameterSchema = z.object({ - path: z.string().describe("Path to the file to read"), + path: z.string().describe('Path to the file to read'), range: z .object({ start: z.number(), end: z.number(), }) .optional() - .describe("Range of bytes to read"), + .describe('Range of bytes to read'), maxSize: z .number() .optional() .describe( - "Maximum size to read, prevents reading arbitrarily large files that blow up the context window", + 'Maximum size to read, prevents reading arbitrarily large files that blow up the context window', ), description: z .string() .max(80) - .describe("The reason you are reading this file (max 80 chars)"), + .describe('The reason you are reading this file (max 80 chars)'), }); const returnSchema = z.object({ @@ -43,8 +43,8 @@ type Parameters = z.infer; type ReturnType = z.infer; export const readFileTool: Tool = { - name: "readFile", - description: "Reads file content within size limits and optional range", + name: 'readFile', + description: 'Reads file content within size limits and optional range', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ({ path: filePath, range, maxSize = OUTPUT_LIMIT }) => { @@ -70,7 +70,7 @@ export const readFileTool: Tool = { ); return { path: filePath, - content: buffer.toString("utf8", 0, bytesRead), + content: buffer.toString('utf8', 0, bytesRead), size: stats.size, range, }; @@ -81,7 +81,7 @@ export const readFileTool: Tool = { return { path: filePath, - content: await fs.readFile(absolutePath, "utf8"), + content: await fs.readFile(absolutePath, 'utf8'), size: stats.size, }; }, diff --git a/src/tools/io/updateFile.test.ts b/src/tools/io/updateFile.test.ts index 6d7280f..1ad597e 100644 --- a/src/tools/io/updateFile.test.ts +++ b/src/tools/io/updateFile.test.ts @@ -1,32 +1,32 @@ -/* eslint-disable max-lines-per-function */ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { join } from "path"; -import { randomUUID } from "crypto"; -import { mkdtemp } from "fs/promises"; -import { tmpdir } from "os"; -import { updateFileTool } from "./updateFile.js"; -import { readFileTool } from "./readFile.js"; -import { shellExecuteTool } from "../system/shellExecute.js"; -import { MockLogger } from "../../utils/mockLogger.js"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { join } from 'path'; +import { randomUUID } from 'crypto'; +import { mkdtemp } from 'fs/promises'; +import { tmpdir } from 'os'; +import { updateFileTool } from './updateFile.js'; +import { readFileTool } from './readFile.js'; +import { shellExecuteTool } from '../system/shellExecute.js'; +import { MockLogger } from '../../utils/mockLogger.js'; const logger = new MockLogger(); -describe("updateFile", () => { +// eslint-disable-next-line max-lines-per-function +describe('updateFile', () => { let testDir: string; beforeEach(async () => { - testDir = await mkdtemp(join(tmpdir(), "updatefile-test-")); + testDir = await mkdtemp(join(tmpdir(), 'updatefile-test-')); }); afterEach(async () => { await shellExecuteTool.execute( - { command: `rm -rf "${testDir}"`, description: "test" }, + { command: `rm -rf "${testDir}"`, description: 'test' }, { logger }, ); }); it("should rewrite a file's content", async () => { - const testContent = "test content"; + const testContent = 'test content'; const testPath = join(testDir, `${randomUUID()}.txt`); // Create and rewrite the file @@ -34,29 +34,29 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: testContent, }, - description: "test", + description: 'test', }, { logger }, ); // Verify return value expect(result.path).toBe(testPath); - expect(result.operation).toBe("rewrite"); + expect(result.operation).toBe('rewrite'); // Verify content const readResult = await readFileTool.execute( - { path: testPath, description: "test" }, + { path: testPath, description: 'test' }, { logger }, ); expect(readResult.content).toBe(testContent); }); - it("should append content to a file", async () => { - const initialContent = "initial content\n"; - const appendContent = "appended content"; + it('should append content to a file', async () => { + const initialContent = 'initial content\n'; + const appendContent = 'appended content'; const expectedContent = initialContent + appendContent; const testPath = join(testDir, `${randomUUID()}.txt`); @@ -65,10 +65,10 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: initialContent, }, - description: "test", + description: 'test', }, { logger }, ); @@ -78,31 +78,31 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "append", + command: 'append', content: appendContent, }, - description: "test", + description: 'test', }, { logger }, ); // Verify return value expect(result.path).toBe(testPath); - expect(result.operation).toBe("append"); + expect(result.operation).toBe('append'); // Verify content const readResult = await readFileTool.execute( - { path: testPath, description: "test" }, + { path: testPath, description: 'test' }, { logger }, ); expect(readResult.content).toBe(expectedContent); }); - it("should update specific text in a file", async () => { - const initialContent = "Hello world! This is a test."; - const oldStr = "world"; - const newStr = "universe"; - const expectedContent = "Hello universe! This is a test."; + it('should update specific text in a file', async () => { + const initialContent = 'Hello world! This is a test.'; + const oldStr = 'world'; + const newStr = 'universe'; + const expectedContent = 'Hello universe! This is a test.'; const testPath = join(testDir, `${randomUUID()}.txt`); // Create initial file @@ -110,10 +110,10 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: initialContent, }, - description: "test", + description: 'test', }, { logger }, ); @@ -123,31 +123,31 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "update", + command: 'update', oldStr, newStr, }, - description: "test", + description: 'test', }, { logger }, ); // Verify return value expect(result.path).toBe(testPath); - expect(result.operation).toBe("update"); + expect(result.operation).toBe('update'); // Verify content const readResult = await readFileTool.execute( - { path: testPath, description: "test" }, + { path: testPath, description: 'test' }, { logger }, ); expect(readResult.content).toBe(expectedContent); }); - it("should throw error when update finds multiple occurrences", async () => { - const initialContent = "Hello world! This is a world test."; - const oldStr = "world"; - const newStr = "universe"; + it('should throw error when update finds multiple occurrences', async () => { + const initialContent = 'Hello world! This is a world test.'; + const oldStr = 'world'; + const newStr = 'universe'; const testPath = join(testDir, `${randomUUID()}.txt`); // Create initial file @@ -155,10 +155,10 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: initialContent, }, - description: "test", + description: 'test', }, { logger }, ); @@ -169,41 +169,41 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "update", + command: 'update', oldStr, newStr, }, - description: "test", + description: 'test', }, { logger }, ), - ).rejects.toThrow("Found 2 occurrences of oldStr, expected exactly 1"); + ).rejects.toThrow('Found 2 occurrences of oldStr, expected exactly 1'); }); it("should create parent directories if they don't exist", async () => { - const testContent = "test content"; - const nestedPath = join(testDir, "nested", "dir", `${randomUUID()}.txt`); + const testContent = 'test content'; + const nestedPath = join(testDir, 'nested', 'dir', `${randomUUID()}.txt`); // Create file in nested directory const result = await updateFileTool.execute( { path: nestedPath, operation: { - command: "rewrite", + command: 'rewrite', content: testContent, }, - description: "test", + description: 'test', }, { logger }, ); // Verify return value expect(result.path).toBe(nestedPath); - expect(result.operation).toBe("rewrite"); + expect(result.operation).toBe('rewrite'); // Verify content const readResult = await readFileTool.execute( - { path: nestedPath, description: "test" }, + { path: nestedPath, description: 'test' }, { logger }, ); expect(readResult.content).toBe(testContent); diff --git a/src/tools/io/updateFile.ts b/src/tools/io/updateFile.ts index a5b7905..3329819 100644 --- a/src/tools/io/updateFile.ts +++ b/src/tools/io/updateFile.ts @@ -1,46 +1,46 @@ -import * as fs from "fs/promises"; -import * as path from "path"; -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -const updateOperationSchema = z.discriminatedUnion("command", [ +const updateOperationSchema = z.discriminatedUnion('command', [ z.object({ - command: z.literal("update"), - oldStr: z.string().describe("Existing text to replace (must be unique)"), - newStr: z.string().describe("New text to insert"), + command: z.literal('update'), + oldStr: z.string().describe('Existing text to replace (must be unique)'), + newStr: z.string().describe('New text to insert'), }), z.object({ - command: z.literal("rewrite"), - content: z.string().describe("Complete new file content"), + command: z.literal('rewrite'), + content: z.string().describe('Complete new file content'), }), z.object({ - command: z.literal("append"), - content: z.string().describe("Content to append to file"), + command: z.literal('append'), + content: z.string().describe('Content to append to file'), }), ]); const parameterSchema = z.object({ - path: z.string().describe("Path to the file"), - operation: updateOperationSchema.describe("Update operation to perform"), + path: z.string().describe('Path to the file'), + operation: updateOperationSchema.describe('Update operation to perform'), description: z .string() .max(80) - .describe("The reason you are modifying this file (max 80 chars)"), + .describe('The reason you are modifying this file (max 80 chars)'), }); const returnSchema = z.object({ - path: z.string().describe("Path to the updated file"), - operation: z.enum(["update", "rewrite", "append"]), + path: z.string().describe('Path to the updated file'), + operation: z.enum(['update', 'rewrite', 'append']), }); type Parameters = z.infer; type ReturnType = z.infer; export const updateFileTool: Tool = { - name: "updateFile", + name: 'updateFile', description: - "Creates a file or updates a file by rewriting, patching, or appending content", + 'Creates a file or updates a file by rewriting, patching, or appending content', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ({ path: filePath, operation }, { logger }) => { @@ -49,8 +49,8 @@ export const updateFileTool: Tool = { await fs.mkdir(path.dirname(absolutePath), { recursive: true }); - if (operation.command === "update") { - const content = await fs.readFile(absolutePath, "utf8"); + if (operation.command === 'update') { + const content = await fs.readFile(absolutePath, 'utf8'); const occurrences = content.split(operation.oldStr).length - 1; if (occurrences !== 1) { throw new Error( @@ -60,12 +60,12 @@ export const updateFileTool: Tool = { await fs.writeFile( absolutePath, content.replace(operation.oldStr, operation.newStr), - "utf8", + 'utf8', ); - } else if (operation.command === "append") { - await fs.appendFile(absolutePath, operation.content, "utf8"); + } else if (operation.command === 'append') { + await fs.appendFile(absolutePath, operation.content, 'utf8'); } else { - await fs.writeFile(absolutePath, operation.content, "utf8"); + await fs.writeFile(absolutePath, operation.content, 'utf8'); } logger.verbose(`Operation complete: ${operation.command}`); diff --git a/src/tools/system/sequenceComplete.ts b/src/tools/system/sequenceComplete.ts index b4a6634..e521d33 100644 --- a/src/tools/system/sequenceComplete.ts +++ b/src/tools/system/sequenceComplete.ts @@ -1,26 +1,24 @@ -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; const parameterSchema = z.object({ - result: z.string().describe("The final result to return from the tool agent"), + result: z.string().describe('The final result to return from the tool agent'), }); const returnSchema = z .string() - .describe("This is returned to the caller of the tool agent."); + .describe('This is returned to the caller of the tool agent.'); type Parameters = z.infer; type ReturnType = z.infer; export const sequenceCompleteTool: Tool = { - name: "sequenceComplete", - description: "Completes the tool use sequence and returns the final result", + name: 'sequenceComplete', + description: 'Completes the tool use sequence and returns the final result', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), - execute: async ({ result }) => { - return result; - }, + execute: ({ result }) => Promise.resolve(result), logParameters: () => {}, logReturns: (output, { logger }) => { logger.info(`Completed: ${output}`); diff --git a/src/tools/system/shellExecute.test.ts b/src/tools/system/shellExecute.test.ts index bff247a..603a854 100644 --- a/src/tools/system/shellExecute.test.ts +++ b/src/tools/system/shellExecute.test.ts @@ -1,23 +1,23 @@ -import { describe, it, expect } from "vitest"; -import { shellExecuteTool } from "./shellExecute.js"; -import { MockLogger } from "../../utils/mockLogger.js"; +import { describe, it, expect } from 'vitest'; +import { shellExecuteTool } from './shellExecute.js'; +import { MockLogger } from '../../utils/mockLogger.js'; const logger = new MockLogger(); -describe("shellExecute", () => { - it("should execute shell commands", async () => { +describe('shellExecute', () => { + it('should execute shell commands', async () => { const { stdout } = await shellExecuteTool.execute( - { command: "echo 'test'", description: "test" }, + { command: "echo 'test'", description: 'test' }, { logger }, ); - expect(stdout).toContain("test"); + expect(stdout).toContain('test'); }); - it("should handle command errors", async () => { + it('should handle command errors', async () => { const { error } = await shellExecuteTool.execute( - { command: "nonexistentcommand", description: "test" }, + { command: 'nonexistentcommand', description: 'test' }, { logger }, ); - expect(error).toContain("Command failed:"); + expect(error).toContain('Command failed:'); }); }); diff --git a/src/tools/system/shellExecute.ts b/src/tools/system/shellExecute.ts index 98355c6..58e4789 100644 --- a/src/tools/system/shellExecute.ts +++ b/src/tools/system/shellExecute.ts @@ -1,23 +1,24 @@ -import { exec, ExecException } from "child_process"; -import { promisify } from "util"; -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { exec, ExecException } from 'child_process'; +import { promisify } from 'util'; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { errorToString } from '../../utils/errorToString.js'; const execAsync = promisify(exec); const parameterSchema = z.object({ command: z .string() - .describe("The shell command to execute in MacOS bash format"), + .describe('The shell command to execute in MacOS bash format'), description: z .string() .max(80) - .describe("The reason this shell command is being run (max 80 chars)"), + .describe('The reason this shell command is being run (max 80 chars)'), timeout: z .number() .optional() - .describe("Timeout in milliseconds (optional, default 30000)"), + .describe('Timeout in milliseconds (optional, default 30000)'), }); const returnSchema = z @@ -29,7 +30,7 @@ const returnSchema = z error: z.string().optional(), }) .describe( - "Command execution results including stdout, stderr, and exit code", + 'Command execution results including stdout, stderr, and exit code', ); type Parameters = z.infer; @@ -41,9 +42,9 @@ interface ExtendedExecException extends ExecException { } export const shellExecuteTool: Tool = { - name: "shellExecute", + name: 'shellExecute', description: - "Executes a bash shell command and returns its output, can do amazing things if you are a shell scripting wizard", + 'Executes a bash shell command and returns its output, can do amazing things if you are a shell scripting wizard', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -61,7 +62,7 @@ export const shellExecuteTool: Tool = { maxBuffer: 10 * 1024 * 1024, // 10MB buffer }); - logger.verbose("Command executed successfully"); + logger.verbose('Command executed successfully'); logger.verbose(`stdout: ${stdout.trim()}`); if (stderr.trim()) { logger.verbose(`stderr: ${stderr.trim()}`); @@ -71,31 +72,33 @@ export const shellExecuteTool: Tool = { stdout: stdout.trim(), stderr: stderr.trim(), code: 0, - error: "", + error: '', command, }; } catch (error) { if (error instanceof Error) { const execError = error as ExtendedExecException; - const isTimeout = error.message.includes("timeout"); + const isTimeout = error.message.includes('timeout'); logger.verbose(`Command execution failed: ${error.message}`); return { error: isTimeout - ? "Command execution timed out after " + timeout + "ms" + ? 'Command execution timed out after ' + timeout + 'ms' : error.message, - stdout: (execError.stdout as string | undefined)?.trim() ?? "", - stderr: (execError.stderr as string | undefined)?.trim() ?? "", + stdout: execError.stdout?.trim() ?? '', + stderr: execError.stderr?.trim() ?? '', code: execError.code ?? -1, command, }; } - logger.error(`Unknown error occurred during command execution: ${error}`); + logger.error( + `Unknown error occurred during command execution: ${errorToString(error)}`, + ); return { - error: `Unknown error occurred: ${error}`, - stdout: "", - stderr: "", + error: `Unknown error occurred: ${errorToString(error)}`, + stdout: '', + stderr: '', code: -1, command, }; diff --git a/src/tools/system/shellMessage.test.ts b/src/tools/system/shellMessage.test.ts index e597ea1..daaf725 100644 --- a/src/tools/system/shellMessage.test.ts +++ b/src/tools/system/shellMessage.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { processStates, shellStartTool } from "./shellStart.js"; -import { MockLogger } from "../../utils/mockLogger.js"; -import { shellMessageTool, NodeSignals } from "./shellMessage.js"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { processStates, shellStartTool } from './shellStart.js'; +import { MockLogger } from '../../utils/mockLogger.js'; +import { shellMessageTool, NodeSignals } from './shellMessage.js'; const logger = new MockLogger(); @@ -9,15 +9,15 @@ const logger = new MockLogger(); const getInstanceId = ( result: Awaited>, ) => { - if (result.mode === "async") { + if (result.mode === 'async') { return result.instanceId; } - throw new Error("Expected async mode result"); + throw new Error('Expected async mode result'); }; // eslint-disable-next-line max-lines-per-function -describe("shellMessageTool", () => { - let testInstanceId = ""; +describe('shellMessageTool', () => { + let testInstanceId = ''; beforeEach(() => { processStates.clear(); @@ -30,12 +30,12 @@ describe("shellMessageTool", () => { processStates.clear(); }); - it("should interact with a running process", async () => { + it('should interact with a running process', async () => { // Start a test process - force async mode with timeout const startResult = await shellStartTool.execute( { - command: "cat", // cat will echo back input - description: "Test interactive process", + command: 'cat', // cat will echo back input + description: 'Test interactive process', timeout: 50, // Force async mode for interactive process }, { logger }, @@ -47,22 +47,22 @@ describe("shellMessageTool", () => { const result = await shellMessageTool.execute( { instanceId: testInstanceId, - stdin: "hello world", - description: "Test interaction", + stdin: 'hello world', + description: 'Test interaction', }, { logger }, ); - expect(result.stdout).toBe("hello world"); - expect(result.stderr).toBe(""); + expect(result.stdout).toBe('hello world'); + expect(result.stderr).toBe(''); expect(result.completed).toBe(false); }); - it("should handle nonexistent process", async () => { + it('should handle nonexistent process', async () => { const result = await shellMessageTool.execute( { - instanceId: "nonexistent-id", - description: "Test invalid process", + instanceId: 'nonexistent-id', + description: 'Test invalid process', }, { logger }, ); @@ -71,12 +71,12 @@ describe("shellMessageTool", () => { expect(result.completed).toBe(false); }); - it("should handle process completion", async () => { + it('should handle process completion', async () => { // Start a quick process - force async mode const startResult = await shellStartTool.execute( { command: 'echo "test" && sleep 0.1', - description: "Test completion", + description: 'Test completion', timeout: 0, // Force async mode }, { logger }, @@ -90,7 +90,7 @@ describe("shellMessageTool", () => { const result = await shellMessageTool.execute( { instanceId, - description: "Check completion", + description: 'Check completion', }, { logger }, ); @@ -100,12 +100,12 @@ describe("shellMessageTool", () => { expect(processStates.has(instanceId)).toBe(true); }); - it("should handle SIGTERM signal correctly", async () => { + it('should handle SIGTERM signal correctly', async () => { // Start a long-running process const startResult = await shellStartTool.execute( { - command: "sleep 10", - description: "Test SIGTERM handling", + command: 'sleep 10', + description: 'Test SIGTERM handling', timeout: 0, // Force async mode }, { logger }, @@ -117,7 +117,7 @@ describe("shellMessageTool", () => { { instanceId, signal: NodeSignals.SIGTERM, - description: "Send SIGTERM", + description: 'Send SIGTERM', }, { logger }, ); @@ -128,7 +128,7 @@ describe("shellMessageTool", () => { const result2 = await shellMessageTool.execute( { instanceId, - description: "Check on status", + description: 'Check on status', }, { logger }, ); @@ -137,12 +137,12 @@ describe("shellMessageTool", () => { expect(result2.error).toBeUndefined(); }); - it("should handle signals on terminated process gracefully", async () => { + it('should handle signals on terminated process gracefully', async () => { // Start a process const startResult = await shellStartTool.execute( { - command: "sleep 1", - description: "Test signal handling on terminated process", + command: 'sleep 1', + description: 'Test signal handling on terminated process', timeout: 0, // Force async mode }, { logger }, @@ -155,7 +155,7 @@ describe("shellMessageTool", () => { { instanceId, signal: NodeSignals.SIGTERM, - description: "Send signal to terminated process", + description: 'Send signal to terminated process', }, { logger }, ); @@ -164,12 +164,12 @@ describe("shellMessageTool", () => { expect(result.completed).toBe(true); }); - it("should verify signaled flag after process termination", async () => { + it('should verify signaled flag after process termination', async () => { // Start a process const startResult = await shellStartTool.execute( { - command: "sleep 5", - description: "Test signal flag verification", + command: 'sleep 5', + description: 'Test signal flag verification', timeout: 0, // Force async mode }, { logger }, @@ -182,7 +182,7 @@ describe("shellMessageTool", () => { { instanceId, signal: NodeSignals.SIGTERM, - description: "Send SIGTERM", + description: 'Send SIGTERM', }, { logger }, ); @@ -193,7 +193,7 @@ describe("shellMessageTool", () => { const checkResult = await shellMessageTool.execute( { instanceId, - description: "Check signal state", + description: 'Check signal state', }, { logger }, ); diff --git a/src/tools/system/shellMessage.ts b/src/tools/system/shellMessage.ts index d91a610..64ac397 100644 --- a/src/tools/system/shellMessage.ts +++ b/src/tools/system/shellMessage.ts @@ -1,57 +1,57 @@ -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { processStates } from "./shellStart.js"; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { processStates } from './shellStart.js'; // Define NodeJS signals as an enum export enum NodeSignals { - SIGABRT = "SIGABRT", - SIGALRM = "SIGALRM", - SIGBUS = "SIGBUS", - SIGCHLD = "SIGCHLD", - SIGCONT = "SIGCONT", - SIGFPE = "SIGFPE", - SIGHUP = "SIGHUP", - SIGILL = "SIGILL", - SIGINT = "SIGINT", - SIGIO = "SIGIO", - SIGIOT = "SIGIOT", - SIGKILL = "SIGKILL", - SIGPIPE = "SIGPIPE", - SIGPOLL = "SIGPOLL", - SIGPROF = "SIGPROF", - SIGPWR = "SIGPWR", - SIGQUIT = "SIGQUIT", - SIGSEGV = "SIGSEGV", - SIGSTKFLT = "SIGSTKFLT", - SIGSTOP = "SIGSTOP", - SIGSYS = "SIGSYS", - SIGTERM = "SIGTERM", - SIGTRAP = "SIGTRAP", - SIGTSTP = "SIGTSTP", - SIGTTIN = "SIGTTIN", - SIGTTOU = "SIGTTOU", - SIGUNUSED = "SIGUNUSED", - SIGURG = "SIGURG", - SIGUSR1 = "SIGUSR1", - SIGUSR2 = "SIGUSR2", - SIGVTALRM = "SIGVTALRM", - SIGWINCH = "SIGWINCH", - SIGXCPU = "SIGXCPU", - SIGXFSZ = "SIGXFSZ" + SIGABRT = 'SIGABRT', + SIGALRM = 'SIGALRM', + SIGBUS = 'SIGBUS', + SIGCHLD = 'SIGCHLD', + SIGCONT = 'SIGCONT', + SIGFPE = 'SIGFPE', + SIGHUP = 'SIGHUP', + SIGILL = 'SIGILL', + SIGINT = 'SIGINT', + SIGIO = 'SIGIO', + SIGIOT = 'SIGIOT', + SIGKILL = 'SIGKILL', + SIGPIPE = 'SIGPIPE', + SIGPOLL = 'SIGPOLL', + SIGPROF = 'SIGPROF', + SIGPWR = 'SIGPWR', + SIGQUIT = 'SIGQUIT', + SIGSEGV = 'SIGSEGV', + SIGSTKFLT = 'SIGSTKFLT', + SIGSTOP = 'SIGSTOP', + SIGSYS = 'SIGSYS', + SIGTERM = 'SIGTERM', + SIGTRAP = 'SIGTRAP', + SIGTSTP = 'SIGTSTP', + SIGTTIN = 'SIGTTIN', + SIGTTOU = 'SIGTTOU', + SIGUNUSED = 'SIGUNUSED', + SIGURG = 'SIGURG', + SIGUSR1 = 'SIGUSR1', + SIGUSR2 = 'SIGUSR2', + SIGVTALRM = 'SIGVTALRM', + SIGWINCH = 'SIGWINCH', + SIGXCPU = 'SIGXCPU', + SIGXFSZ = 'SIGXFSZ', } const parameterSchema = z.object({ - instanceId: z.string().describe("The ID returned by shellStart"), - stdin: z.string().optional().describe("Input to send to process"), + instanceId: z.string().describe('The ID returned by shellStart'), + stdin: z.string().optional().describe('Input to send to process'), signal: z .nativeEnum(NodeSignals) .optional() - .describe("Signal to send to the process (e.g., SIGTERM, SIGINT)"), + .describe('Signal to send to the process (e.g., SIGTERM, SIGINT)'), description: z .string() .max(80) - .describe("The reason for this shell interaction (max 80 chars)"), + .describe('The reason for this shell interaction (max 80 chars)'), }); const returnSchema = z @@ -63,16 +63,16 @@ const returnSchema = z signaled: z.boolean().optional(), }) .describe( - "Process interaction results including stdout, stderr, and completion status", + 'Process interaction results including stdout, stderr, and completion status', ); type Parameters = z.infer; type ReturnType = z.infer; export const shellMessageTool: Tool = { - name: "shellMessage", + name: 'shellMessage', description: - "Interacts with a running shell process, sending input and receiving output", + 'Interacts with a running shell process, sending input and receiving output', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -81,7 +81,7 @@ export const shellMessageTool: Tool = { { logger }, ): Promise => { logger.verbose( - `Interacting with shell process ${instanceId}${stdin ? " with input" : ""}${signal ? ` with signal ${signal}` : ""}`, + `Interacting with shell process ${instanceId}${stdin ? ' with input' : ''}${signal ? ` with signal ${signal}` : ''}`, ); try { @@ -95,8 +95,8 @@ export const shellMessageTool: Tool = { const wasKilled = processState.process.kill(signal); if (!wasKilled) { return { - stdout: "", - stderr: "", + stdout: '', + stderr: '', completed: processState.state.completed, signaled: false, error: `Failed to send signal ${signal} to process (process may have already terminated)`, @@ -108,7 +108,7 @@ export const shellMessageTool: Tool = { // Send input if provided if (stdin) { if (!processState.process.stdin?.writable) { - throw new Error("Process stdin is not available"); + throw new Error('Process stdin is not available'); } processState.process.stdin.write(`${stdin}\n`); } @@ -117,14 +117,14 @@ export const shellMessageTool: Tool = { await new Promise((resolve) => setTimeout(resolve, 100)); // Get accumulated output - const stdout = processState.stdout.join(""); - const stderr = processState.stderr.join(""); + const stdout = processState.stdout.join(''); + const stderr = processState.stderr.join(''); // Clear the buffers processState.stdout = []; processState.stderr = []; - logger.verbose("Interaction completed successfully"); + logger.verbose('Interaction completed successfully'); if (stdout) { logger.verbose(`stdout: ${stdout.trim()}`); } @@ -143,8 +143,8 @@ export const shellMessageTool: Tool = { logger.verbose(`Process interaction failed: ${error.message}`); return { - stdout: "", - stderr: "", + stdout: '', + stderr: '', completed: false, error: error.message, }; @@ -153,8 +153,8 @@ export const shellMessageTool: Tool = { const errorMessage = String(error); logger.error(`Unknown error during process interaction: ${errorMessage}`); return { - stdout: "", - stderr: "", + stdout: '', + stderr: '', completed: false, error: `Unknown error occurred: ${errorMessage}`, }; diff --git a/src/tools/system/shellStart.test.ts b/src/tools/system/shellStart.test.ts index 84a033d..ba0ce1b 100644 --- a/src/tools/system/shellStart.test.ts +++ b/src/tools/system/shellStart.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { processStates, shellStartTool } from "./shellStart.js"; -import { MockLogger } from "../../utils/mockLogger.js"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { processStates, shellStartTool } from './shellStart.js'; +import { MockLogger } from '../../utils/mockLogger.js'; const logger = new MockLogger(); -describe("shellStartTool", () => { +describe('shellStartTool', () => { beforeEach(() => { processStates.clear(); }); @@ -16,103 +16,103 @@ describe("shellStartTool", () => { processStates.clear(); }); - it("should handle fast commands in sync mode", async () => { + it('should handle fast commands in sync mode', async () => { const result = await shellStartTool.execute( { command: 'echo "test"', - description: "Test process", + description: 'Test process', timeout: 500, // Generous timeout to ensure sync mode }, - { logger } + { logger }, ); - expect(result.mode).toBe("sync"); - if (result.mode === "sync") { + expect(result.mode).toBe('sync'); + if (result.mode === 'sync') { expect(result.exitCode).toBe(0); - expect(result.stdout).toBe("test"); + expect(result.stdout).toBe('test'); expect(result.error).toBeUndefined(); } }); - it("should switch to async mode for slow commands", async () => { + it('should switch to async mode for slow commands', async () => { const result = await shellStartTool.execute( { - command: "sleep 1", - description: "Slow command test", + command: 'sleep 1', + description: 'Slow command test', timeout: 50, // Short timeout to force async mode }, - { logger } + { logger }, ); - expect(result.mode).toBe("async"); - if (result.mode === "async") { + expect(result.mode).toBe('async'); + if (result.mode === 'async') { expect(result.instanceId).toBeDefined(); expect(result.error).toBeUndefined(); } }); - it("should handle invalid commands with sync error", async () => { + it('should handle invalid commands with sync error', async () => { const result = await shellStartTool.execute( { - command: "nonexistentcommand", - description: "Invalid command test", + command: 'nonexistentcommand', + description: 'Invalid command test', }, - { logger } + { logger }, ); - expect(result.mode).toBe("sync"); - if (result.mode === "sync") { + expect(result.mode).toBe('sync'); + if (result.mode === 'sync') { expect(result.exitCode).not.toBe(0); expect(result.error).toBeDefined(); } }); - it("should keep process in processStates in both modes", async () => { + it('should keep process in processStates in both modes', async () => { // Test sync mode const syncResult = await shellStartTool.execute( { command: 'echo "test"', - description: "Sync completion test", + description: 'Sync completion test', timeout: 500, }, - { logger } + { logger }, ); // Even sync results should be in processStates expect(processStates.size).toBeGreaterThan(0); - expect(syncResult.mode).toBe("sync"); + expect(syncResult.mode).toBe('sync'); expect(syncResult.error).toBeUndefined(); - if (syncResult.mode === "sync") { + if (syncResult.mode === 'sync') { expect(syncResult.exitCode).toBe(0); } // Test async mode const asyncResult = await shellStartTool.execute( { - command: "sleep 1", - description: "Async completion test", + command: 'sleep 1', + description: 'Async completion test', timeout: 50, }, - { logger } + { logger }, ); - if (asyncResult.mode === "async") { + if (asyncResult.mode === 'async') { expect(processStates.has(asyncResult.instanceId)).toBe(true); } }); - it("should handle piped commands correctly in async mode", async () => { + it('should handle piped commands correctly in async mode', async () => { const result = await shellStartTool.execute( { command: 'grep "test"', - description: "Pipe test", + description: 'Pipe test', timeout: 50, // Force async for interactive command }, - { logger } + { logger }, ); - expect(result.mode).toBe("async"); - if (result.mode === "async") { + expect(result.mode).toBe('async'); + if (result.mode === 'async') { expect(result.instanceId).toBeDefined(); expect(result.error).toBeUndefined(); @@ -120,30 +120,30 @@ describe("shellStartTool", () => { expect(processState).toBeDefined(); if (processState?.process.stdin) { - processState.process.stdin.write("this is a test line\n"); - processState.process.stdin.write("not matching line\n"); - processState.process.stdin.write("another test here\n"); + processState.process.stdin.write('this is a test line\n'); + processState.process.stdin.write('not matching line\n'); + processState.process.stdin.write('another test here\n'); processState.process.stdin.end(); // Wait for output await new Promise((resolve) => setTimeout(resolve, 200)); // Check stdout in processState - expect(processState.stdout.join("")).toContain("test"); - expect(processState.stdout.join("")).not.toContain("not matching"); + expect(processState.stdout.join('')).toContain('test'); + expect(processState.stdout.join('')).not.toContain('not matching'); } } }); - it("should use default timeout of 10000ms", async () => { + it('should use default timeout of 10000ms', async () => { const result = await shellStartTool.execute( { - command: "sleep 1", - description: "Default timeout test", + command: 'sleep 1', + description: 'Default timeout test', }, - { logger } + { logger }, ); - expect(result.mode).toBe("sync"); + expect(result.mode).toBe('sync'); }); }); diff --git a/src/tools/system/shellStart.ts b/src/tools/system/shellStart.ts index ca84991..c4d1db9 100644 --- a/src/tools/system/shellStart.ts +++ b/src/tools/system/shellStart.ts @@ -1,9 +1,10 @@ -import { spawn } from "child_process"; -import type { ChildProcess } from "child_process"; -import { Tool } from "../../core/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { v4 as uuidv4 } from "uuid"; +import { spawn } from 'child_process'; +import type { ChildProcess } from 'child_process'; +import { Tool } from '../../core/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { v4 as uuidv4 } from 'uuid'; +import { errorToString } from '../../utils/errorToString.js'; // Define ProcessState type type ProcessState = { @@ -21,40 +22,40 @@ type ProcessState = { export const processStates: Map = new Map(); const parameterSchema = z.object({ - command: z.string().describe("The shell command to execute"), + command: z.string().describe('The shell command to execute'), description: z .string() .max(80) - .describe("The reason this shell command is being run (max 80 chars)"), + .describe('The reason this shell command is being run (max 80 chars)'), timeout: z .number() .optional() .describe( - "Timeout in ms before switching to async mode (default: 10s, which usually is sufficient)" + 'Timeout in ms before switching to async mode (default: 10s, which usually is sufficient)', ), }); const returnSchema = z.union([ z .object({ - mode: z.literal("sync"), + mode: z.literal('sync'), stdout: z.string(), stderr: z.string(), exitCode: z.number(), error: z.string().optional(), }) .describe( - "Synchronous execution results when command completes within timeout" + 'Synchronous execution results when command completes within timeout', ), z .object({ - mode: z.literal("async"), + mode: z.literal('async'), instanceId: z.string(), stdout: z.string(), stderr: z.string(), error: z.string().optional(), }) - .describe("Asynchronous execution results when command exceeds timeout"), + .describe('Asynchronous execution results when command exceeds timeout'), ]); type Parameters = z.infer; @@ -63,15 +64,15 @@ type ReturnType = z.infer; const DEFAULT_TIMEOUT = 1000 * 10; export const shellStartTool: Tool = { - name: "shellStart", + name: 'shellStart', description: - "Starts a shell command with fast sync mode (default 100ms timeout) that falls back to async mode for longer-running commands", + 'Starts a shell command with fast sync mode (default 100ms timeout) that falls back to async mode for longer-running commands', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ( { command, timeout = DEFAULT_TIMEOUT }, - { logger } + { logger }, ): Promise => { logger.verbose(`Starting shell command: ${command}`); @@ -96,37 +97,37 @@ export const shellStartTool: Tool = { // Handle process events if (process.stdout) - process.stdout.on("data", (data) => { + process.stdout.on('data', (data) => { const output = data.toString(); processState.stdout.push(output); logger.verbose(`[${instanceId}] stdout: ${output.trim()}`); }); if (process.stderr) - process.stderr.on("data", (data) => { + process.stderr.on('data', (data) => { const output = data.toString(); processState.stderr.push(output); logger.verbose(`[${instanceId}] stderr: ${output.trim()}`); }); - process.on("error", (error) => { + process.on('error', (error) => { logger.error(`[${instanceId}] Process error: ${error.message}`); processState.state.completed = true; if (!hasResolved) { hasResolved = true; resolve({ - mode: "async", + mode: 'async', instanceId, - stdout: processState.stdout.join("").trim(), - stderr: processState.stderr.join("").trim(), + stdout: processState.stdout.join('').trim(), + stderr: processState.stderr.join('').trim(), error: error.message, }); } }); - process.on("exit", (code, signal) => { + process.on('exit', (code, signal) => { logger.verbose( - `[${instanceId}] Process exited with code ${code} and signal ${signal}` + `[${instanceId}] Process exited with code ${code} and signal ${signal}`, ); processState.state.completed = true; @@ -138,12 +139,12 @@ export const shellStartTool: Tool = { // If we haven't resolved yet, this happened within the timeout // so return sync results resolve({ - mode: "sync", - stdout: processState.stdout.join("").trim(), - stderr: processState.stderr.join("").trim(), + mode: 'sync', + stdout: processState.stdout.join('').trim(), + stderr: processState.stderr.join('').trim(), exitCode: code ?? 1, ...(code !== 0 && { - error: `Process exited with code ${code}${signal ? ` and signal ${signal}` : ""}`, + error: `Process exited with code ${code}${signal ? ` and signal ${signal}` : ''}`, }), }); } @@ -154,19 +155,19 @@ export const shellStartTool: Tool = { if (!hasResolved) { hasResolved = true; resolve({ - mode: "async", + mode: 'async', instanceId, - stdout: processState.stdout.join("").trim(), - stderr: processState.stderr.join("").trim(), + stdout: processState.stdout.join('').trim(), + stderr: processState.stderr.join('').trim(), }); } }, timeout); } catch (error) { - logger.error(`Failed to start process: ${error}`); + logger.error(`Failed to start process: ${errorToString(error)}`); resolve({ - mode: "sync", - stdout: "", - stderr: "", + mode: 'sync', + stdout: '', + stderr: '', exitCode: 1, error: error instanceof Error ? error.message : String(error), }); @@ -176,14 +177,14 @@ export const shellStartTool: Tool = { logParameters: ( { command, description, timeout = DEFAULT_TIMEOUT }, - { logger } + { logger }, ) => { logger.info( - `Starting "${command}", ${description} (timeout: ${timeout}ms)` + `Starting "${command}", ${description} (timeout: ${timeout}ms)`, ); }, logReturns: (output, { logger }) => { - if (output.mode === "async") { + if (output.mode === 'async') { logger.info(`Process started with instance ID: ${output.instanceId}`); } else { logger.info(`Process completed with exit code: ${output.exitCode}`); diff --git a/src/types/browserTools.ts b/src/types/browserTools.ts index 2a0b15a..b96bf37 100644 --- a/src/types/browserTools.ts +++ b/src/types/browserTools.ts @@ -22,10 +22,12 @@ export const BrowseStartOptionsSchema = z.object({ headless: z.boolean().optional(), slowMo: z.number().optional(), timeout: z.number().optional(), - viewport: z.object({ - width: z.number(), - height: z.number() - }).optional() + viewport: z + .object({ + width: z.number(), + height: z.number(), + }) + .optional(), }); export interface BrowseStartParams { @@ -37,7 +39,7 @@ export interface BrowseStartParams { export const BrowseStartParamsSchema = z.object({ browserType: BrowserTypeEnum, options: BrowseStartOptionsSchema.optional(), - description: z.string().max(80) + description: z.string().max(80), }); export interface BrowseStartResult { @@ -50,7 +52,7 @@ export interface BrowseStartResult { export interface BrowseMessageActionParams { navigate?: { url: string }; click?: { selector: string }; - type?: { + type?: { selector: string; text: string; }; @@ -60,11 +62,13 @@ export interface BrowseMessageActionParams { export const BrowseMessageActionParamsSchema = z.object({ navigate: z.object({ url: z.string().url() }).optional(), click: z.object({ selector: z.string() }).optional(), - type: z.object({ - selector: z.string(), - text: z.string() - }).optional(), - end: z.boolean().optional() + type: z + .object({ + selector: z.string(), + text: z.string(), + }) + .optional(), + end: z.boolean().optional(), }); export interface BrowseMessageParams { @@ -78,7 +82,7 @@ export const BrowseMessageParamsSchema = z.object({ sessionId: z.string(), action: BrowserActionEnum, actionParams: BrowseMessageActionParamsSchema, - description: z.string().max(80) + description: z.string().max(80), }); export interface BrowseMessageResult { @@ -86,4 +90,4 @@ export interface BrowseMessageResult { content?: string; consoleLogs?: string[]; error?: string; -} \ No newline at end of file +} diff --git a/src/utils/errorToString.ts b/src/utils/errorToString.ts new file mode 100644 index 0000000..3b3798e --- /dev/null +++ b/src/utils/errorToString.ts @@ -0,0 +1,7 @@ +export const errorToString = (error: unknown): string => { + if (error instanceof Error) { + return `${error.constructor.name}: ${error.message}`; + } else { + return JSON.stringify(error); + } +}; diff --git a/src/utils/logger.test.ts b/src/utils/logger.test.ts index 604487a..4319b2e 100644 --- a/src/utils/logger.test.ts +++ b/src/utils/logger.test.ts @@ -1,17 +1,16 @@ - -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { Logger, LogLevel } from "./logger.js"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { Logger, LogLevel } from './logger.js'; -describe("Logger", () => { +describe('Logger', () => { let consoleSpy: { [key: string]: any }; beforeEach(() => { // Setup console spies before each test consoleSpy = { - log: vi.spyOn(console, "log").mockImplementation(() => {}), - info: vi.spyOn(console, "log").mockImplementation(() => {}), - warn: vi.spyOn(console, "warn").mockImplementation(() => {}), - error: vi.spyOn(console, "error").mockImplementation(() => {}), + log: vi.spyOn(console, 'log').mockImplementation(() => {}), + info: vi.spyOn(console, 'log').mockImplementation(() => {}), + warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), + error: vi.spyOn(console, 'error').mockImplementation(() => {}), }; }); @@ -20,89 +19,89 @@ describe("Logger", () => { vi.clearAllMocks(); }); - describe("Basic logging functionality", () => { - const logger = new Logger({ name: "TestLogger", logLevel: LogLevel.debug }); - const testMessage = "Test message"; + describe('Basic logging functionality', () => { + const logger = new Logger({ name: 'TestLogger', logLevel: LogLevel.debug }); + const testMessage = 'Test message'; - it("should log debug messages", () => { + it('should log debug messages', () => { logger.debug(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); }); - it("should log verbose messages", () => { + it('should log verbose messages', () => { logger.verbose(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); }); - it("should log info messages", () => { + it('should log info messages', () => { logger.info(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); }); - it("should log warning messages", () => { + it('should log warning messages', () => { logger.warn(testMessage); expect(consoleSpy.warn).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); }); - it("should log error messages", () => { + it('should log error messages', () => { logger.error(testMessage); expect(consoleSpy.error).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); }); }); - describe("Nested logger functionality", () => { + describe('Nested logger functionality', () => { const parentLogger = new Logger({ - name: "ParentLogger", + name: 'ParentLogger', logLevel: LogLevel.debug, }); const childLogger = new Logger({ - name: "ChildLogger", + name: 'ChildLogger', parent: parentLogger, logLevel: LogLevel.debug, }); - const testMessage = "Nested test message"; + const testMessage = 'Nested test message'; - it("should include proper indentation for nested loggers", () => { + it('should include proper indentation for nested loggers', () => { childLogger.info(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(" ") // Two spaces of indentation + expect.stringContaining(' '), // Two spaces of indentation ); }); - it("should properly log messages at all levels with nested logger", () => { + it('should properly log messages at all levels with nested logger', () => { childLogger.debug(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); childLogger.verbose(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); childLogger.info(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); childLogger.warn(testMessage); expect(consoleSpy.warn).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); childLogger.error(testMessage); expect(consoleSpy.error).toHaveBeenCalledWith( - expect.stringContaining(testMessage) + expect.stringContaining(testMessage), ); }); }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index fbeeb90..539b6ec 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,4 +1,4 @@ -import chalk, { ChalkInstance } from "chalk"; +import chalk, { ChalkInstance } from 'chalk'; export enum LogLevel { debug = 0, @@ -34,7 +34,7 @@ export const BasicLoggerStyler = { ): string => level === LogLevel.debug || level === LogLevel.verbose ? chalk.dim(prefix) - : "", + : '', }; const loggerStyle = BasicLoggerStyler; @@ -73,20 +73,21 @@ export class Logger { currentParent = currentParent.parent; } - this.prefix = " ".repeat(offsetSpaces); + this.prefix = ' '.repeat(offsetSpaces); } - private toStrings(messages: any[]) { + private toStrings(messages: unknown[]) { return messages .map((message) => - typeof message === "object" + typeof message === 'object' ? JSON.stringify(message, null, 2) - : String(message), + : // eslint-disable-next-line @typescript-eslint/no-base-to-string + String(message), ) - .join(" "); + .join(' '); } - private formatMessages(level: LogLevel, messages: any[]): string { + private formatMessages(level: LogLevel, messages: unknown[]): string { const formatted = this.toStrings(messages); const messageColor = loggerStyle.getColor(level, this.nesting); const prefix = loggerStyle.formatPrefix( @@ -96,32 +97,32 @@ export class Logger { ); return formatted - .split("\n") + .split('\n') .map((line) => `${this.prefix}${prefix} ${messageColor(line)}`) - .join("\n"); + .join('\n'); } - debug(...messages: any[]): void { + debug(...messages: unknown[]): void { if (this.logLevelIndex > LogLevel.debug) return; console.log(this.formatMessages(LogLevel.debug, messages)); } - verbose(...messages: any[]): void { + verbose(...messages: unknown[]): void { if (this.logLevelIndex > LogLevel.verbose) return; console.log(this.formatMessages(LogLevel.verbose, messages)); } - info(...messages: any[]): void { + info(...messages: unknown[]): void { if (this.logLevelIndex > LogLevel.info) return; console.log(this.formatMessages(LogLevel.info, messages)); } - warn(...messages: any[]): void { + warn(...messages: unknown[]): void { if (this.logLevelIndex > LogLevel.warn) return; console.warn(this.formatMessages(LogLevel.warn, messages)); } - error(...messages: any[]): void { + error(...messages: unknown[]): void { console.error(this.formatMessages(LogLevel.error, messages)); } } diff --git a/src/utils/mockLogger.ts b/src/utils/mockLogger.ts index efa31ed..e7bda6f 100644 --- a/src/utils/mockLogger.ts +++ b/src/utils/mockLogger.ts @@ -1,8 +1,8 @@ -import { Logger } from "./logger.js"; +import { Logger } from './logger.js'; export class MockLogger extends Logger { constructor() { - super({ name: "mock" }); + super({ name: 'mock' }); } debug(..._messages: any[]): void {} diff --git a/src/utils/stringifyLimited.test.ts b/src/utils/stringifyLimited.test.ts index 27156cc..b85435b 100644 --- a/src/utils/stringifyLimited.test.ts +++ b/src/utils/stringifyLimited.test.ts @@ -1,19 +1,19 @@ -import { describe, it, expect } from "vitest"; -import { stringify2 } from "./stringifyLimited.js"; +import { describe, it, expect } from 'vitest'; +import { stringify2 } from './stringifyLimited.js'; -describe("stringify2", () => { - it("should stringify simple objects", async () => { - const obj = { a: 1, b: "test" }; +describe('stringify2', () => { + it('should stringify simple objects', () => { + const obj = { a: 1, b: 'test' }; const result = stringify2(obj); const parsed = JSON.parse(result); - expect(parsed).toEqual({ a: "1", b: '"test"' }); + expect(parsed).toEqual({ a: '1', b: '"test"' }); }); - it("should handle nested objects", async () => { + it('should handle nested objects', () => { const obj = { a: 1, b: { - c: "test", + c: 'test', d: [1, 2, 3], }, }; @@ -23,39 +23,39 @@ describe("stringify2", () => { expect(parsed.b).toBeTruthy(); }); - it("should truncate long values", async () => { - const longString = "x".repeat(2000); + it('should truncate long values', () => { + const longString = 'x'.repeat(2000); const obj = { str: longString }; const result = stringify2(obj, 100); const parsed = JSON.parse(result); expect(parsed.str.length <= 100).toBeTruthy(); }); - it("should handle null and undefined", async () => { + it('should handle null and undefined', () => { const obj = { nullValue: null, undefinedValue: undefined, }; const result = stringify2(obj); const parsed = JSON.parse(result); - expect(parsed.nullValue).toBe("null"); + expect(parsed.nullValue).toBe('null'); expect(parsed.undefinedValue).toBe(undefined); }); - it("should handle arrays", async () => { + it('should handle arrays', () => { const obj = { - arr: [1, "test", { nested: true }], + arr: [1, 'test', { nested: true }], }; const result = stringify2(obj); const parsed = JSON.parse(result); expect(parsed.arr).toBeTruthy(); }); - it("should handle Date objects", async () => { - const date = new Date("2024-01-01"); + it('should handle Date objects', () => { + const date = new Date('2024-01-01'); const obj = { date }; const result = stringify2(obj); const parsed = JSON.parse(result); - expect(parsed.date.includes("2024-01-01")).toBeTruthy(); + expect(parsed.date.includes('2024-01-01')).toBeTruthy(); }); }); diff --git a/src/utils/stringifyLimited.ts b/src/utils/stringifyLimited.ts index a76b500..e058f04 100644 --- a/src/utils/stringifyLimited.ts +++ b/src/utils/stringifyLimited.ts @@ -8,7 +8,7 @@ export const stringify2 = ( .map(([key, val]) => [ key, val === null - ? "null" + ? 'null' : JSON.stringify(val, null, 2).slice(0, valueCharacterLimit), ]), ); diff --git a/src/utils/versionCheck.test.ts b/src/utils/versionCheck.test.ts index 0f892bb..26e3362 100644 --- a/src/utils/versionCheck.test.ts +++ b/src/utils/versionCheck.test.ts @@ -1,42 +1,41 @@ - -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { generateUpgradeMessage, fetchLatestVersion, getPackageInfo, checkForUpdates, -} from "./versionCheck.js"; -import * as fs from "fs"; -import * as fsPromises from "fs/promises"; -import * as path from "path"; -import { getSettingsDir } from "../settings/settings.js"; - -vi.mock("fs"); -vi.mock("fs/promises"); -vi.mock("../settings/settings.js"); - -describe("versionCheck", () => { - describe("generateUpgradeMessage", () => { - it("returns null when versions are the same", () => { - expect(generateUpgradeMessage("1.0.0", "1.0.0", "test-package")).toBe( - null +} from './versionCheck.js'; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; +import { getSettingsDir } from '../settings/settings.js'; + +vi.mock('fs'); +vi.mock('fs/promises'); +vi.mock('../settings/settings.js'); + +describe('versionCheck', () => { + describe('generateUpgradeMessage', () => { + it('returns null when versions are the same', () => { + expect(generateUpgradeMessage('1.0.0', '1.0.0', 'test-package')).toBe( + null, ); }); - it("returns upgrade message when versions differ", () => { - const message = generateUpgradeMessage("1.0.0", "1.1.0", "test-package"); - expect(message).toContain("Update available: 1.0.0 → 1.1.0"); + it('returns upgrade message when versions differ', () => { + const message = generateUpgradeMessage('1.0.0', '1.1.0', 'test-package'); + expect(message).toContain('Update available: 1.0.0 → 1.1.0'); expect(message).toContain("Run 'npm install -g test-package' to update"); }); - it("returns null when current version is higher", () => { - expect(generateUpgradeMessage("2.0.0", "1.0.0", "test-package")).toBe( - null + it('returns null when current version is higher', () => { + expect(generateUpgradeMessage('2.0.0', '1.0.0', 'test-package')).toBe( + null, ); }); }); - describe("fetchLatestVersion", () => { + describe('fetchLatestVersion', () => { const mockFetch = vi.fn(); const originalFetch = global.fetch; @@ -49,57 +48,57 @@ describe("versionCheck", () => { vi.clearAllMocks(); }); - it("returns version when fetch succeeds", async () => { + it('returns version when fetch succeeds', async () => { mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve({ version: "1.1.0" }), + json: () => Promise.resolve({ version: '1.1.0' }), }); - const version = await fetchLatestVersion("test-package"); - expect(version).toBe("1.1.0"); + const version = await fetchLatestVersion('test-package'); + expect(version).toBe('1.1.0'); expect(mockFetch).toHaveBeenCalledWith( - "https://registry.npmjs.org/test-package/latest" + 'https://registry.npmjs.org/test-package/latest', ); }); - it("throws error when fetch fails", async () => { + it('throws error when fetch fails', async () => { mockFetch.mockResolvedValueOnce({ ok: false, - statusText: "Not Found", + statusText: 'Not Found', }); - await expect(fetchLatestVersion("test-package")).rejects.toThrow( - "Failed to fetch version info: Not Found" + await expect(fetchLatestVersion('test-package')).rejects.toThrow( + 'Failed to fetch version info: Not Found', ); }); - it("throws error when version is missing from response", async () => { + it('throws error when version is missing from response', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}), }); - await expect(fetchLatestVersion("test-package")).rejects.toThrow( - "Version info not found in response" + await expect(fetchLatestVersion('test-package')).rejects.toThrow( + 'Version info not found in response', ); }); }); - describe("getPackageInfo", () => { - it("returns package info from package.json", () => { + describe('getPackageInfo', () => { + it('returns package info from package.json', () => { const info = getPackageInfo(); - expect(info).toHaveProperty("name"); - expect(info).toHaveProperty("version"); - expect(typeof info.name).toBe("string"); - expect(typeof info.version).toBe("string"); + expect(info).toHaveProperty('name'); + expect(info).toHaveProperty('version'); + expect(typeof info.name).toBe('string'); + expect(typeof info.version).toBe('string'); }); }); - describe("checkForUpdates", () => { + describe('checkForUpdates', () => { const mockFetch = vi.fn(); const originalFetch = global.fetch; - const mockSettingsDir = "/mock/settings/dir"; - const versionFilePath = path.join(mockSettingsDir, "lastVersionCheck"); + const mockSettingsDir = '/mock/settings/dir'; + const versionFilePath = path.join(mockSettingsDir, 'lastVersionCheck'); beforeEach(() => { global.fetch = mockFetch; @@ -112,11 +111,11 @@ describe("versionCheck", () => { vi.clearAllMocks(); }); - it("returns null and initiates background check when no cached version", async () => { + it('returns null and initiates background check when no cached version', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve({ version: "2.0.0" }), + json: () => Promise.resolve({ version: '2.0.0' }), }); const result = await checkForUpdates(); @@ -128,30 +127,30 @@ describe("versionCheck", () => { expect(mockFetch).toHaveBeenCalled(); expect(fsPromises.writeFile).toHaveBeenCalledWith( versionFilePath, - "2.0.0", - "utf8" + '2.0.0', + 'utf8', ); }); - it("returns upgrade message when cached version is newer", async () => { + it('returns upgrade message when cached version is newer', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fsPromises.readFile).mockResolvedValue("2.0.0"); + vi.mocked(fsPromises.readFile).mockResolvedValue('2.0.0'); const result = await checkForUpdates(); - expect(result).toContain("Update available"); + expect(result).toContain('Update available'); }); - it("handles errors gracefully during version check", async () => { + it('handles errors gracefully during version check', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fsPromises.readFile).mockRejectedValue(new Error("Test error")); + vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('Test error')); const result = await checkForUpdates(); expect(result).toBe(null); }); - it("handles errors gracefully during background update", async () => { + it('handles errors gracefully during background update', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); - mockFetch.mockRejectedValue(new Error("Network error")); + mockFetch.mockRejectedValue(new Error('Network error')); const result = await checkForUpdates(); expect(result).toBe(null); diff --git a/src/utils/versionCheck.ts b/src/utils/versionCheck.ts index 6af26ef..3d38467 100644 --- a/src/utils/versionCheck.ts +++ b/src/utils/versionCheck.ts @@ -1,23 +1,23 @@ -import { Logger } from "./logger.js"; -import chalk from "chalk"; -import { createRequire } from "module"; -import * as path from "path"; -import type { PackageJson } from "type-fest"; -import { getSettingsDir } from "../settings/settings.js"; -import * as fsPromises from "fs/promises"; -import * as fs from "fs"; -import semver from "semver"; +import { Logger } from './logger.js'; +import chalk from 'chalk'; +import { createRequire } from 'module'; +import * as path from 'path'; +import type { PackageJson } from 'type-fest'; +import { getSettingsDir } from '../settings/settings.js'; +import * as fsPromises from 'fs/promises'; +import * as fs from 'fs'; +import * as semver from 'semver'; const require = createRequire(import.meta.url); -const logger = new Logger({ name: "version-check" }); +const logger = new Logger({ name: 'version-check' }); export function getPackageInfo(): { name: string; version: string; } { - const packageInfo = require("../../package.json") as PackageJson; + const packageInfo = require('../../package.json') as PackageJson; if (!packageInfo.name || !packageInfo.version) { - throw new Error("Unable to determine package info"); + throw new Error('Unable to determine package info'); } return { @@ -36,7 +36,7 @@ export async function fetchLatestVersion(packageName: string): Promise { const data = (await response.json()) as { version: string | undefined }; if (!data.version) { - throw new Error("Version info not found in response"); + throw new Error('Version info not found in response'); } return data.version; } @@ -44,11 +44,11 @@ export async function fetchLatestVersion(packageName: string): Promise { export function generateUpgradeMessage( currentVersion: string, latestVersion: string, - packageName: string + packageName: string, ): string | null { return semver.gt(latestVersion, currentVersion) ? chalk.green( - ` Update available: ${currentVersion} → ${latestVersion}\n Run 'npm install -g ${packageName}' to update` + ` Update available: ${currentVersion} → ${latestVersion}\n Run 'npm install -g ${packageName}' to update`, ) : null; } @@ -58,27 +58,27 @@ export async function checkForUpdates(): Promise { const { name: packageName, version: currentVersion } = getPackageInfo(); const settingDir = getSettingsDir(); - const versionFilePath = path.join(settingDir, "lastVersionCheck"); + const versionFilePath = path.join(settingDir, 'lastVersionCheck'); if (fs.existsSync(versionFilePath)) { const lastVersionCheck = await fsPromises.readFile( versionFilePath, - "utf8" + 'utf8', ); return generateUpgradeMessage( currentVersion, lastVersionCheck, - packageName + packageName, ); } fetchLatestVersion(packageName) .then(async (latestVersion) => { - return fsPromises.writeFile(versionFilePath, latestVersion, "utf8"); + return fsPromises.writeFile(versionFilePath, latestVersion, 'utf8'); }) .catch((error) => { logger.warn( - "Error fetching latest version:", - error instanceof Error ? error.message : String(error) + 'Error fetching latest version:', + error instanceof Error ? error.message : String(error), ); }); @@ -86,8 +86,8 @@ export async function checkForUpdates(): Promise { } catch (error) { // Log error but don't throw to handle gracefully logger.warn( - "Error checking for updates:", - error instanceof Error ? error.message : String(error) + 'Error checking for updates:', + error instanceof Error ? error.message : String(error), ); return null; } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..9dbaf76 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src", "test"] +}