diff --git a/.DS_Store b/.DS_Store index 813178d..3eeb03c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550047f..7a71c26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - "*" + - '*' pull_request: branches: - main diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dc75c8a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/README.md b/README.md index 400ae03..46cb003 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ pnpm install pnpm build # Run locally built cli in interactive mode -pnpm -f mycoder -i +pnpm cli -i ``` ## 📦 Packages diff --git a/eslint.config.js b/eslint.config.js index 142221d..31d13cb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,80 +1,80 @@ // eslint.config.js -import js from "@eslint/js"; -import pluginImport from "eslint-plugin-import"; -import prettierRecommended from "eslint-plugin-prettier/recommended"; -import pluginPromise from "eslint-plugin-promise"; -import pluginUnusedImports from "eslint-plugin-unused-imports"; -import ts from "typescript-eslint"; +import js from '@eslint/js'; +import pluginImport from 'eslint-plugin-import'; +import prettierRecommended from 'eslint-plugin-prettier/recommended'; +import pluginPromise from 'eslint-plugin-promise'; +import pluginUnusedImports from 'eslint-plugin-unused-imports'; +import ts from 'typescript-eslint'; export default ts.config( js.configs.recommended, ts.configs.recommended, prettierRecommended, - pluginPromise.configs["flat/recommended"], + pluginPromise.configs['flat/recommended'], { plugins: { import: pluginImport, - "unused-imports": pluginUnusedImports, + 'unused-imports': pluginUnusedImports, }, rules: { - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unused-vars": "off", // turned off in favor of unused-imports/no-unused-vars - "@typescript-eslint/no-require-imports": "warn", + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': 'off', // turned off in favor of unused-imports/no-unused-vars + '@typescript-eslint/no-require-imports': 'warn', // Remove unused imports - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ - "error", + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'error', { - vars: "all", - varsIgnorePattern: "^_", - args: "after-used", - argsIgnorePattern: "^_", + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', }, ], // Import organization - "import/order": [ - "error", + 'import/order': [ + 'error', { groups: [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index", - "object", - "type", + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', ], - "newlines-between": "always", - alphabetize: { order: "asc", caseInsensitive: true }, + 'newlines-between': 'always', + alphabetize: { order: 'asc', caseInsensitive: true }, warnOnUnassignedImports: true, }, ], - "import/no-duplicates": "error", + 'import/no-duplicates': 'error', }, settings: { - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"], + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], }, - "import/resolver": { + 'import/resolver': { typescript: { alwaysTryTypes: true, - project: ["./packages/*/tsconfig.json"], + project: ['./packages/*/tsconfig.json'], }, }, }, }, { ignores: [ - "**/dist", - "**/_doNotUse", - "**/node_modules", - "**/.vinxi", - "**/.output", - "**/pnpm-lock.yaml", - "**/routeTree.gen.ts", + '**/dist', + '**/_doNotUse', + '**/node_modules', + '**/.vinxi', + '**/.output', + '**/pnpm-lock.yaml', + '**/routeTree.gen.ts', ], }, ); diff --git a/package.json b/package.json index bf95132..ac410e2 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "gcloud-setup": "gcloud auth application-default login && gcloud config set account \"ben@drivecore.ai\" && gcloud config set project drivecore-primary && gcloud config set run/region us-central1", "changeset": "changeset", "version": "changeset version", - "release": "changeset publish" + "release": "changeset publish", + "cli": "cd packages/cli && node --no-deprecation bin/cli.js" }, "dependencies": { "rimraf": "^6.0.1" diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 7ea6ad8..10aeb30 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -1,6 +1,6 @@ -import { Logger } from "../utils/logger.js"; +import { Logger } from '../utils/logger.js'; -import { Tool, ToolCall } from "./types.js"; +import { Tool, ToolCall } from './types.js'; const OUTPUT_LIMIT = 12 * 1024; // 10KB limit @@ -29,7 +29,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)}`); }); @@ -45,10 +45,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)}`); }); @@ -56,7 +56,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/packages/agent/src/core/toolAgent.respawn.test.ts b/packages/agent/src/core/toolAgent.respawn.test.ts index a8a8334..d032d17 100644 --- a/packages/agent/src/core/toolAgent.respawn.test.ts +++ b/packages/agent/src/core/toolAgent.respawn.test.ts @@ -1,11 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { toolAgent } from "../../src/core/toolAgent.js"; -import { getTools } from "../../src/tools/getTools.js"; -import { Logger } from "../../src/utils/logger.js"; +import { toolAgent } from '../../src/core/toolAgent.js'; +import { getTools } from '../../src/tools/getTools.js'; +import { Logger } from '../../src/utils/logger.js'; // Mock Anthropic SDK -vi.mock("@anthropic-ai/sdk", () => { +vi.mock('@anthropic-ai/sdk', () => { return { default: vi.fn().mockImplementation(() => ({ messages: { @@ -14,10 +14,10 @@ vi.mock("@anthropic-ai/sdk", () => { .mockResolvedValueOnce({ content: [ { - type: "tool_use", - name: "respawn", - id: "test-id", - input: { respawnContext: "new context" }, + type: 'tool_use', + name: 'respawn', + id: 'test-id', + input: { respawnContext: 'new context' }, }, ], usage: { input_tokens: 10, output_tokens: 10 }, @@ -31,26 +31,26 @@ vi.mock("@anthropic-ai/sdk", () => { }; }); -describe("toolAgent respawn functionality", () => { - const mockLogger = new Logger({ name: "test" }); +describe('toolAgent respawn functionality', () => { + const mockLogger = new Logger({ name: 'test' }); const tools = getTools(); beforeEach(() => { - process.env.ANTHROPIC_API_KEY = "test-key"; + process.env.ANTHROPIC_API_KEY = 'test-key'; vi.clearAllMocks(); }); - it("should handle respawn tool calls", async () => { - const result = await toolAgent("initial prompt", tools, mockLogger, { + it('should handle respawn tool calls', async () => { + const result = await toolAgent('initial prompt', tools, mockLogger, { maxIterations: 2, // Need at least 2 iterations for respawn + empty response - model: "test-model", + model: 'test-model', maxTokens: 100, temperature: 0, - getSystemPrompt: () => "test system prompt", + getSystemPrompt: () => 'test system prompt', }); expect(result.result).toBe( - "Maximum sub-agent iterations reach without successful completion", + 'Maximum sub-agent iterations reach without successful completion', ); }); }); diff --git a/packages/agent/src/core/toolAgent.test.ts b/packages/agent/src/core/toolAgent.test.ts index 6f62394..b94db05 100644 --- a/packages/agent/src/core/toolAgent.test.ts +++ b/packages/agent/src/core/toolAgent.test.ts @@ -1,41 +1,41 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { MockLogger } from "../utils/mockLogger.js"; +import { MockLogger } from '../utils/mockLogger.js'; -import { executeToolCall } from "./executeToolCall.js"; -import { toolAgent } from "./toolAgent.js"; -import { Tool } from "./types.js"; +import { executeToolCall } from './executeToolCall.js'; +import { toolAgent } from './toolAgent.js'; +import { Tool } from './types.js'; const logger = new MockLogger(); // Mock configuration for testing const testConfig = { maxIterations: 50, - model: "claude-3-7-sonnet-latest", + model: 'claude-3-7-sonnet-latest', maxTokens: 4096, temperature: 0.7, - getSystemPrompt: () => "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 }, - model: "claude-3-7-sonnet-latest", - role: "assistant", - id: "msg_123", + model: 'claude-3-7-sonnet-latest', + role: 'assistant', + id: 'msg_123', }; // Mock Anthropic SDK const mockCreate = vi.fn().mockImplementation(() => mockResponse); -vi.mock("@anthropic-ai/sdk", () => ({ +vi.mock('@anthropic-ai/sdk', () => ({ default: class { messages = { create: mockCreate, @@ -43,9 +43,9 @@ vi.mock("@anthropic-ai/sdk", () => ({ }, })); -describe("toolAgent", () => { +describe('toolAgent', () => { beforeEach(() => { - process.env.ANTHROPIC_API_KEY = "test-key"; + process.env.ANTHROPIC_API_KEY = 'test-key'; }); afterEach(() => { @@ -54,65 +54,65 @@ 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: ({ 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: ({ 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, ); - 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], @@ -121,39 +121,39 @@ describe("toolAgent", () => { ).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: () => { - throw new Error("Deliberate failure"); + throw new Error('Deliberate failure'); }, }; await expect( executeToolCall( { - id: "1", - name: "errorTool", + id: '1', + name: 'errorTool', input: {}, }, [errorTool], logger, ), - ).rejects.toThrow("Deliberate failure"); + ).rejects.toThrow('Deliberate failure'); }); // Test empty response handling - it("should handle empty responses by sending a reminder", async () => { + it('should handle empty responses by sending a reminder', async () => { // Reset the mock and set up the sequence of responses mockCreate.mockReset(); mockCreate @@ -164,7 +164,7 @@ describe("toolAgent", () => { .mockResolvedValueOnce(mockResponse); const result = await toolAgent( - "Test prompt", + 'Test prompt', [sequenceCompleteTool], logger, testConfig, @@ -172,23 +172,23 @@ describe("toolAgent", () => { // Verify that create was called twice (once for empty response, once for completion) expect(mockCreate).toHaveBeenCalledTimes(2); - expect(result.result).toBe("Test complete"); + expect(result.result).toBe('Test complete'); }); // New tests for async system prompt - it("should handle async system prompt", async () => { + it('should handle async system prompt', async () => { // Reset mock and set expected response mockCreate.mockReset(); mockCreate.mockResolvedValue(mockResponse); const result = await toolAgent( - "Test prompt", + 'Test prompt', [sequenceCompleteTool], logger, 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/packages/agent/src/core/toolAgent.ts b/packages/agent/src/core/toolAgent.ts index 41570a8..6c7260f 100644 --- a/packages/agent/src/core/toolAgent.ts +++ b/packages/agent/src/core/toolAgent.ts @@ -1,18 +1,18 @@ -import { execSync } from "child_process"; +import { execSync } from 'child_process'; -import Anthropic from "@anthropic-ai/sdk"; +import Anthropic from '@anthropic-ai/sdk'; -import { getAnthropicApiKeyError } from "../utils/errors.js"; -import { Logger } from "../utils/logger.js"; +import { getAnthropicApiKeyError } from '../utils/errors.js'; +import { Logger } from '../utils/logger.js'; -import { executeToolCall } from "./executeToolCall.js"; +import { executeToolCall } from './executeToolCall.js'; import { Tool, TextContent, ToolUseContent, ToolResultContent, Message, -} from "./types.js"; +} from './types.js'; export interface ToolAgentResult { result: string; @@ -25,7 +25,7 @@ export interface ToolAgentResult { const CONFIG = { maxIterations: 50, - model: "claude-3-7-sonnet-latest", + model: 'claude-3-7-sonnet-latest', maxTokens: 4096, temperature: 0.7, getSystemPrompt: () => { @@ -39,48 +39,48 @@ 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", - "", - "Feel free to use Google and Bing via the browser tools to search for information or for ideas when you get stuck.", - "", - "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', + '', + 'Feel free to use Google and Bing via the browser tools to search for information or for ideas when you get stuck.', + '', + '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'); }, }; @@ -95,11 +95,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, @@ -126,15 +126,15 @@ async function executeTools( logger.verbose(`Executing ${toolCalls.length} tool calls`); // Check for respawn tool call - const respawnCall = toolCalls.find((call) => call.name === "respawn"); + const respawnCall = toolCalls.find((call) => call.name === 'respawn'); if (respawnCall) { return { sequenceCompleted: false, toolResults: [ { - type: "tool_result", + type: 'tool_result', tool_use_id: respawnCall.id, - content: "Respawn initiated", + content: 'Respawn initiated', }, ], respawn: { @@ -145,7 +145,7 @@ async function executeTools( const results = await Promise.all( toolCalls.map(async (call) => { - let toolResult = ""; + let toolResult = ''; try { toolResult = await executeToolCall(call, tools, logger, { workingDirectory, @@ -154,10 +154,10 @@ async function executeTools( 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', }; }), ); @@ -171,10 +171,10 @@ 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 }; @@ -187,8 +187,8 @@ export const toolAgent = async ( config = CONFIG, workingDirectory?: string, ): 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; @@ -200,12 +200,12 @@ 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 = config.getSystemPrompt(); @@ -229,18 +229,18 @@ 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) { // Instead of treating empty response as completion, remind the agent - logger.verbose("Received empty response from agent, sending reminder"); + logger.verbose('Received empty response from agent, sending reminder'); messages.push({ - role: "user", + role: 'user', content: [ { - type: "text", - text: "I notice you sent an empty response. If you are done with your tasks, please call the sequenceComplete tool with your results. If you are waiting for other tools to complete, you can use the sleep tool to wait before checking again.", + type: 'text', + text: 'I notice you sent an empty response. If you are done with your tasks, please call the sequenceComplete tool with your results. If you are waiting for other tools to complete, you can use the sleep tool to wait before checking again.', }, ], }); @@ -254,13 +254,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") + .filter((c) => c.type === 'text') .map((c) => c.text) - .join("\\n"); + .join('\\n'); if (assistantMessage) { logger.info(assistantMessage); } @@ -274,12 +274,12 @@ export const toolAgent = async ( ); if (respawn) { - logger.info("Respawning agent with new context"); + logger.info('Respawning agent with new context'); // Reset messages to just the new context messages.length = 0; messages.push({ - role: "user", - content: [{ type: "text", text: respawn.context }], + role: 'user', + content: [{ type: 'text', text: respawn.context }], }); continue; } @@ -288,7 +288,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, @@ -302,9 +302,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/packages/agent/src/core/toolContext.ts b/packages/agent/src/core/toolContext.ts index b10da57..18b0577 100644 --- a/packages/agent/src/core/toolContext.ts +++ b/packages/agent/src/core/toolContext.ts @@ -1,6 +1,6 @@ -import { Logger } from "../utils/logger.js"; +import { Logger } from '../utils/logger.js'; -import { ToolContext } from "./types.js"; +import { ToolContext } from './types.js'; export function createToolContext( logger: Logger, diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index 4e17e89..c44ba50 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -1,6 +1,6 @@ -import { JsonSchema7Type } from "zod-to-json-schema"; +import { JsonSchema7Type } from 'zod-to-json-schema'; -import { Logger } from "../utils/logger.js"; +import { Logger } from '../utils/logger.js'; export type ToolContext = { logger: Logger; @@ -26,16 +26,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; @@ -45,13 +45,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/packages/agent/src/index.ts b/packages/agent/src/index.ts index cdd60a6..b898f07 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -1,39 +1,39 @@ // Tools - IO -export * from "./tools/io/readFile.js"; -export * from "./tools/io/updateFile.js"; -export * from "./tools/io/fetch.js"; +export * from './tools/io/readFile.js'; +export * from './tools/io/updateFile.js'; +export * from './tools/io/fetch.js'; // Tools - System -export * from "./tools/system/shellStart.js"; -export * from "./tools/system/sleep.js"; -export * from "./tools/system/respawn.js"; -export * from "./tools/system/sequenceComplete.js"; -export * from "./tools/system/shellMessage.js"; -export * from "./tools/system/shellExecute.js"; +export * from './tools/system/shellStart.js'; +export * from './tools/system/sleep.js'; +export * from './tools/system/respawn.js'; +export * from './tools/system/sequenceComplete.js'; +export * from './tools/system/shellMessage.js'; +export * from './tools/system/shellExecute.js'; // Tools - Browser -export * from "./tools/browser/BrowserManager.js"; -export * from "./tools/browser/types.js"; -export * from "./tools/browser/browseMessage.js"; -export * from "./tools/browser/browseStart.js"; -export * from "./tools/browser/PageController.js"; -export * from "./tools/browser/BrowserAutomation.js"; +export * from './tools/browser/BrowserManager.js'; +export * from './tools/browser/types.js'; +export * from './tools/browser/browseMessage.js'; +export * from './tools/browser/browseStart.js'; +export * from './tools/browser/PageController.js'; +export * from './tools/browser/BrowserAutomation.js'; // Tools - Interaction -export * from "./tools/interaction/subAgent.js"; -export * from "./tools/interaction/userPrompt.js"; +export * from './tools/interaction/subAgent.js'; +export * from './tools/interaction/userPrompt.js'; // Core -export * from "./core/toolContext.js"; -export * from "./core/executeToolCall.js"; -export * from "./core/types.js"; -export * from "./core/toolAgent.js"; +export * from './core/toolContext.js'; +export * from './core/executeToolCall.js'; +export * from './core/types.js'; +export * from './core/toolAgent.js'; // Utils -export * from "./tools/getTools.js"; -export * from "./utils/errors.js"; -export * from "./utils/sleep.js"; -export * from "./utils/errorToString.js"; -export * from "./utils/logger.js"; -export * from "./utils/mockLogger.js"; -export * from "./utils/stringifyLimited.js"; +export * from './tools/getTools.js'; +export * from './utils/errors.js'; +export * from './utils/sleep.js'; +export * from './utils/errorToString.js'; +export * from './utils/logger.js'; +export * from './utils/mockLogger.js'; +export * from './utils/stringifyLimited.js'; diff --git a/packages/agent/src/tools/browser/BrowserAutomation.ts b/packages/agent/src/tools/browser/BrowserAutomation.ts index 7190bd0..52f3b83 100644 --- a/packages/agent/src/tools/browser/BrowserAutomation.ts +++ b/packages/agent/src/tools/browser/BrowserAutomation.ts @@ -1,5 +1,5 @@ -import { BrowserManager } from "./BrowserManager.js"; -import { PageController } from "./PageController.js"; +import { BrowserManager } from './BrowserManager.js'; +import { PageController } from './PageController.js'; export class BrowserAutomation { private static instance: BrowserAutomation; diff --git a/packages/agent/src/tools/browser/BrowserManager.ts b/packages/agent/src/tools/browser/BrowserManager.ts index 32c2724..a136e8a 100644 --- a/packages/agent/src/tools/browser/BrowserManager.ts +++ b/packages/agent/src/tools/browser/BrowserManager.ts @@ -1,12 +1,12 @@ -import { chromium } from "@playwright/test"; -import { v4 as uuidv4 } from "uuid"; +import { chromium } from '@playwright/test'; +import { v4 as uuidv4 } from 'uuid'; import { BrowserConfig, BrowserSession, BrowserError, BrowserErrorCode, -} from "./types.js"; +} from './types.js'; export class BrowserManager { private sessions: Map = new Map(); @@ -26,7 +26,7 @@ export class BrowserManager { const context = await browser.newContext({ viewport: null, userAgent: - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }); const page = await context.newPage(); @@ -44,7 +44,7 @@ export class BrowserManager { return session; } catch (error) { throw new BrowserError( - "Failed to create browser session", + 'Failed to create browser session', BrowserErrorCode.LAUNCH_FAILED, error, ); @@ -55,7 +55,7 @@ export class BrowserManager { const session = this.sessions.get(sessionId); if (!session) { throw new BrowserError( - "Session not found", + 'Session not found', BrowserErrorCode.SESSION_ERROR, ); } @@ -67,7 +67,7 @@ export class BrowserManager { this.sessions.delete(sessionId); } catch (error) { throw new BrowserError( - "Failed to close session", + 'Failed to close session', BrowserErrorCode.SESSION_ERROR, error, ); @@ -76,17 +76,17 @@ export class BrowserManager { private setupCleanup(session: BrowserSession): void { // Handle browser disconnection - session.browser.on("disconnected", () => { + session.browser.on('disconnected', () => { this.sessions.delete(session.id); }); // Handle process exit - process.on("exit", () => { + process.on('exit', () => { this.closeSession(session.id).catch(() => {}); }); // Handle unexpected errors - process.on("uncaughtException", () => { + process.on('uncaughtException', () => { this.closeSession(session.id).catch(() => {}); }); } @@ -102,7 +102,7 @@ export class BrowserManager { const session = this.sessions.get(sessionId); if (!session) { throw new BrowserError( - "Session not found", + 'Session not found', BrowserErrorCode.SESSION_ERROR, ); } diff --git a/packages/agent/src/tools/browser/PageController.ts b/packages/agent/src/tools/browser/PageController.ts index c921c1f..2912711 100644 --- a/packages/agent/src/tools/browser/PageController.ts +++ b/packages/agent/src/tools/browser/PageController.ts @@ -1,13 +1,13 @@ -import { Page } from "@playwright/test"; +import { Page } from '@playwright/test'; -import { errorToString } from "../../utils/errorToString.js"; +import { errorToString } from '../../utils/errorToString.js'; import { SelectorType, SelectorOptions, BrowserError, BrowserErrorCode, -} from "./types.js"; +} from './types.js'; export class PageController { constructor(private page: Page) {} @@ -33,7 +33,7 @@ export class PageController { private validateSelector(selector: string, _type: SelectorType): void { if (!selector) { throw new BrowserError( - "Invalid selector: empty string", + 'Invalid selector: empty string', BrowserErrorCode.SELECTOR_INVALID, ); } @@ -50,7 +50,7 @@ export class PageController { this.getSelector(selector, options.type), ); await locator.waitFor({ - state: options.visible ? "visible" : "attached", + state: options.visible ? 'visible' : 'attached', timeout: options.timeout, }); } catch (error) { @@ -107,7 +107,7 @@ export class PageController { const locator = this.page.locator( this.getSelector(selector, options.type), ); - return (await locator.textContent()) || ""; + return (await locator.textContent()) || ''; } catch (error) { throw new BrowserError( `Failed to get text: ${errorToString(error)}`, diff --git a/packages/agent/src/tools/browser/browseMessage.ts b/packages/agent/src/tools/browser/browseMessage.ts index ed14b50..ca0379e 100644 --- a/packages/agent/src/tools/browser/browseMessage.ts +++ b/packages/agent/src/tools/browser/browseMessage.ts @@ -1,31 +1,31 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; -import { errorToString } from "../../utils/errorToString.js"; +import { Tool } from '../../core/types.js'; +import { errorToString } from '../../utils/errorToString.js'; -import { browserSessions, type BrowserAction, SelectorType } from "./types.js"; +import { browserSessions, type BrowserAction, SelectorType } from './types.js'; // Schema for browser action const browserActionSchema = z .object({ - type: z.enum(["goto", "click", "type", "wait", "content", "close"]), + type: z.enum(['goto', 'click', 'type', 'wait', 'content', 'close']), url: z.string().url().optional(), selector: z.string().optional(), selectorType: z.nativeEnum(SelectorType).optional(), text: z.string().optional(), options: z.object({}).optional(), }) - .describe("Browser action to perform"); + .describe('Browser action to perform'); // Main parameter schema const parameterSchema = z.object({ - instanceId: z.string().describe("The ID returned by browseStart"), + instanceId: z.string().describe('The ID returned by browseStart'), action: browserActionSchema, description: z .string() .max(80) - .describe("The reason for this browser action (max 80 chars)"), + .describe('The reason for this browser action (max 80 chars)'), }); // Return schema @@ -51,8 +51,8 @@ const getSelector = (selector: string, type?: SelectorType): string => { }; export const browseMessageTool: Tool = { - name: "browseMessage", - description: "Performs actions in an active browser session", + name: 'browseMessage', + description: 'Performs actions in an active browser session', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -68,19 +68,19 @@ export const browseMessageTool: Tool = { const { page } = session; switch (action.type) { - case "goto": { + case 'goto': { if (!action.url) { - throw new Error("URL required for goto action"); + throw new Error('URL required for goto action'); } - await page.goto(action.url, { waitUntil: "networkidle" }); + await page.goto(action.url, { waitUntil: 'networkidle' }); const content = await page.content(); - logger.verbose("Navigation completed successfully"); - return { status: "success", content }; + logger.verbose('Navigation completed successfully'); + return { status: 'success', content }; } - case "click": { + case 'click': { if (!action.selector) { - throw new Error("Selector required for click action"); + throw new Error('Selector required for click action'); } const clickSelector = getSelector( action.selector, @@ -91,12 +91,12 @@ export const browseMessageTool: Tool = { logger.verbose( `Click action completed on selector: ${clickSelector}`, ); - return { status: "success", content }; + return { status: 'success', content }; } - case "type": { + case 'type': { if (!action.selector || !action.text) { - throw new Error("Selector and text required for type action"); + throw new Error('Selector and text required for type action'); } const typeSelector = getSelector( action.selector, @@ -104,12 +104,12 @@ export const browseMessageTool: Tool = { ); await page.fill(typeSelector, action.text); logger.verbose(`Type action completed on selector: ${typeSelector}`); - return { status: "success" }; + return { status: 'success' }; } - case "wait": { + case 'wait': { if (!action.selector) { - throw new Error("Selector required for wait action"); + throw new Error('Selector required for wait action'); } const waitSelector = getSelector( action.selector, @@ -117,21 +117,21 @@ export const browseMessageTool: Tool = { ); await page.waitForSelector(waitSelector); logger.verbose(`Wait action completed for selector: ${waitSelector}`); - return { status: "success" }; + return { status: 'success' }; } - case "content": { + case 'content': { const content = await page.content(); - logger.verbose("Page content retrieved successfully"); - return { status: "success", content }; + logger.verbose('Page content retrieved successfully'); + return { status: 'success', content }; } - case "close": { + case 'close': { await session.page.context().close(); await session.browser.close(); browserSessions.delete(instanceId); - logger.verbose("Browser session closed successfully"); - return { status: "closed" }; + logger.verbose('Browser session closed successfully'); + return { status: 'closed' }; } default: { @@ -141,9 +141,9 @@ export const browseMessageTool: Tool = { } } } catch (error) { - logger.error("Browser action failed:", { error }); + logger.error('Browser action failed:', { error }); return { - status: "error", + status: 'error', error: errorToString(error), }; } diff --git a/packages/agent/src/tools/browser/browseStart.ts b/packages/agent/src/tools/browser/browseStart.ts index 9166c05..94b7113 100644 --- a/packages/agent/src/tools/browser/browseStart.ts +++ b/packages/agent/src/tools/browser/browseStart.ts @@ -1,27 +1,27 @@ -import { chromium } from "@playwright/test"; -import { v4 as uuidv4 } from "uuid"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { chromium } from '@playwright/test'; +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; -import { errorToString } from "../../utils/errorToString.js"; +import { Tool } from '../../core/types.js'; +import { errorToString } from '../../utils/errorToString.js'; -import { browserSessions } from "./types.js"; +import { browserSessions } from './types.js'; const parameterSchema = z.object({ - url: z.string().url().optional().describe("Initial URL to navigate to"), + url: z.string().url().optional().describe('Initial URL to navigate to'), headless: z .boolean() .optional() - .describe("Run browser in headless mode (default: true)"), + .describe('Run browser in headless mode (default: true)'), timeout: z .number() .optional() - .describe("Default timeout in milliseconds (default: 30000)"), + .describe('Default timeout in milliseconds (default: 30000)'), description: z .string() .max(80) - .describe("The reason for starting this browser session (max 80 chars)"), + .describe('The reason for starting this browser session (max 80 chars)'), }); const returnSchema = z.object({ @@ -35,8 +35,8 @@ type Parameters = z.infer; type ReturnType = z.infer; export const browseStartTool: Tool = { - name: "browseStart", - description: "Starts a new browser session with optional initial URL", + name: 'browseStart', + description: 'Starts a new browser session with optional initial URL', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), @@ -44,7 +44,7 @@ export const browseStartTool: Tool = { { url, headless = true, timeout = 30000 }, { logger }, ): Promise => { - logger.verbose(`Starting browser session${url ? ` at ${url}` : ""}`); + logger.verbose(`Starting browser session${url ? ` at ${url}` : ''}`); try { const instanceId = uuidv4(); @@ -58,7 +58,7 @@ export const browseStartTool: Tool = { const context = await browser.newContext({ viewport: null, userAgent: - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }); // Create new page @@ -75,29 +75,29 @@ export const browseStartTool: Tool = { browserSessions.set(instanceId, session); // Setup cleanup handlers - browser.on("disconnected", () => { + browser.on('disconnected', () => { browserSessions.delete(instanceId); }); // Navigate to URL if provided - let content = ""; + let content = ''; if (url) { - await page.goto(url, { waitUntil: "networkidle" }); + await page.goto(url, { waitUntil: 'networkidle' }); content = await page.content(); } - logger.verbose("Browser session started successfully"); + logger.verbose('Browser session started successfully'); return { instanceId, - status: "initialized", + status: 'initialized', content: content || undefined, }; } catch (error) { logger.error(`Failed to start browser: ${errorToString(error)}`); return { - instanceId: "", - status: "error", + instanceId: '', + status: 'error', error: errorToString(error), }; } @@ -105,7 +105,7 @@ export const browseStartTool: Tool = { logParameters: ({ url, description }, { logger }) => { logger.info( - `Starting browser session${url ? ` at ${url}` : ""}, ${description}`, + `Starting browser session${url ? ` at ${url}` : ''}, ${description}`, ); }, diff --git a/packages/agent/src/tools/browser/browser-manager.test.ts b/packages/agent/src/tools/browser/browser-manager.test.ts index 975be07..dd27635 100644 --- a/packages/agent/src/tools/browser/browser-manager.test.ts +++ b/packages/agent/src/tools/browser/browser-manager.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { BrowserManager } from "./BrowserManager.js"; -import { BrowserError, BrowserErrorCode } from "./types.js"; +import { BrowserManager } from './BrowserManager.js'; +import { BrowserError, BrowserErrorCode } from './types.js'; -describe("BrowserManager", () => { +describe('BrowserManager', () => { let browserManager: BrowserManager; beforeEach(() => { @@ -14,38 +14,38 @@ describe("BrowserManager", () => { await browserManager.closeAllSessions(); }); - describe("createSession", () => { - it("should create a new browser session", async () => { + describe('createSession', () => { + it('should create a new browser session', async () => { const session = await browserManager.createSession(); expect(session.id).toBeDefined(); expect(session.browser).toBeDefined(); expect(session.page).toBeDefined(); }); - it("should create a headless session when specified", async () => { + it('should create a headless session when specified', async () => { const session = await browserManager.createSession({ headless: true }); expect(session.id).toBeDefined(); }); - it("should apply custom timeout when specified", async () => { + it('should apply custom timeout when specified', async () => { const customTimeout = 500; const session = await browserManager.createSession({ defaultTimeout: customTimeout, }); // Verify timeout by attempting to wait for a non-existent element try { - await session.page.waitForSelector("#nonexistent", { + await session.page.waitForSelector('#nonexistent', { timeout: customTimeout - 100, }); } catch (error: any) { - expect(error.message).toContain("imeout"); + expect(error.message).toContain('imeout'); expect(error.message).toContain(`${customTimeout - 100}`); } }); }); - describe("closeSession", () => { - it("should close an existing session", async () => { + describe('closeSession', () => { + it('should close an existing session', async () => { const session = await browserManager.createSession(); await browserManager.closeSession(session.id); @@ -54,25 +54,25 @@ describe("BrowserManager", () => { }).toThrow(BrowserError); }); - it("should throw error when closing non-existent session", async () => { - await expect(browserManager.closeSession("invalid-id")).rejects.toThrow( - new BrowserError("Session not found", BrowserErrorCode.SESSION_ERROR), + it('should throw error when closing non-existent session', async () => { + await expect(browserManager.closeSession('invalid-id')).rejects.toThrow( + new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR), ); }); }); - describe("getSession", () => { - it("should return existing session", async () => { + describe('getSession', () => { + it('should return existing session', async () => { const session = await browserManager.createSession(); const retrieved = browserManager.getSession(session.id); expect(retrieved).toBe(session); }); - it("should throw error for non-existent session", () => { + it('should throw error for non-existent session', () => { expect(() => { - browserManager.getSession("invalid-id"); + browserManager.getSession('invalid-id'); }).toThrow( - new BrowserError("Session not found", BrowserErrorCode.SESSION_ERROR), + new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR), ); }); }); diff --git a/packages/agent/src/tools/browser/element-state.test.ts b/packages/agent/src/tools/browser/element-state.test.ts index f9b6022..aac9c22 100644 --- a/packages/agent/src/tools/browser/element-state.test.ts +++ b/packages/agent/src/tools/browser/element-state.test.ts @@ -6,18 +6,18 @@ import { afterAll, beforeEach, vi, -} from "vitest"; +} from 'vitest'; -import { BrowserManager } from "./BrowserManager.js"; -import { BrowserSession } from "./types.js"; +import { BrowserManager } from './BrowserManager.js'; +import { BrowserSession } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); -describe("Element State Tests", () => { +describe('Element State Tests', () => { let browserManager: BrowserManager; let session: BrowserSession; - const baseUrl = "https://the-internet.herokuapp.com"; + const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { browserManager = new BrowserManager(); @@ -28,12 +28,12 @@ describe("Element State Tests", () => { await browserManager.closeAllSessions(); }); - describe("Checkbox Tests", () => { + describe('Checkbox Tests', () => { beforeEach(async () => { await session.page.goto(`${baseUrl}/checkboxes`); }); - it("should verify initial checkbox states", async () => { + it('should verify initial checkbox states', async () => { const checkboxes = await session.page.$$('input[type="checkbox"]'); expect(checkboxes).toHaveLength(2); @@ -49,10 +49,10 @@ describe("Element State Tests", () => { expect(initialStates[1]).toBe(true); }); - it("should toggle checkbox states", async () => { + it('should toggle checkbox states', async () => { const checkboxes = await session.page.$$('input[type="checkbox"]'); if (!checkboxes[0] || !checkboxes[1]) - throw new Error("Checkboxes not found"); + throw new Error('Checkboxes not found'); // Toggle first checkbox await checkboxes[0].click(); @@ -69,9 +69,9 @@ describe("Element State Tests", () => { expect(secondState).toBe(false); }); - it("should maintain checkbox states after page refresh", async () => { + it('should maintain checkbox states after page refresh', async () => { const checkboxes = await session.page.$$('input[type="checkbox"]'); - if (!checkboxes[0]) throw new Error("First checkbox not found"); + if (!checkboxes[0]) throw new Error('First checkbox not found'); await checkboxes[0].click(); // Toggle first checkbox await session.page.reload(); @@ -91,12 +91,12 @@ describe("Element State Tests", () => { }); }); - describe("Dynamic Controls Tests", () => { + describe('Dynamic Controls Tests', () => { beforeEach(async () => { await session.page.goto(`${baseUrl}/dynamic_controls`); }); - it("should handle enabled/disabled element states", async () => { + it('should handle enabled/disabled element states', async () => { // Wait for the input to be present and verify initial disabled state await session.page.waitForSelector('input[type="text"][disabled]'); @@ -104,8 +104,8 @@ describe("Element State Tests", () => { await session.page.click('button:has-text("Enable")'); // Wait for the message indicating the input is enabled - await session.page.waitForSelector("#message", { - state: "visible", + await session.page.waitForSelector('#message', { + state: 'visible', timeout: 5000, }); @@ -113,12 +113,12 @@ describe("Element State Tests", () => { const input = await session.page.waitForSelector( 'input[type="text"]:not([disabled])', { - state: "visible", + state: 'visible', timeout: 5000, }, ); - if (!input) throw new Error("Enabled input not found"); + if (!input) throw new Error('Enabled input not found'); const isEnabled = await input.evaluate( (el) => !(el as HTMLInputElement).disabled, diff --git a/packages/agent/src/tools/browser/form-interaction.test.ts b/packages/agent/src/tools/browser/form-interaction.test.ts index 04868f0..f331856 100644 --- a/packages/agent/src/tools/browser/form-interaction.test.ts +++ b/packages/agent/src/tools/browser/form-interaction.test.ts @@ -6,18 +6,18 @@ import { afterAll, beforeEach, vi, -} from "vitest"; +} from 'vitest'; -import { BrowserManager } from "./BrowserManager.js"; -import { BrowserSession } from "./types.js"; +import { BrowserManager } from './BrowserManager.js'; +import { BrowserSession } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); -describe("Form Interaction Tests", () => { +describe('Form Interaction Tests', () => { let browserManager: BrowserManager; let session: BrowserSession; - const baseUrl = "https://the-internet.herokuapp.com"; + const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { browserManager = new BrowserManager(); @@ -32,69 +32,69 @@ describe("Form Interaction Tests", () => { await session.page.goto(`${baseUrl}/login`); }); - it("should handle login form with invalid credentials", async () => { - await session.page.type("#username", "invalid_user"); - await session.page.type("#password", "invalid_pass"); + it('should handle login form with invalid credentials', async () => { + await session.page.type('#username', 'invalid_user'); + await session.page.type('#password', 'invalid_pass'); await session.page.click('button[type="submit"]'); - const flashMessage = await session.page.waitForSelector("#flash"); + const flashMessage = await session.page.waitForSelector('#flash'); const messageText = await flashMessage?.evaluate((el) => el.textContent); - expect(messageText).toContain("Your username is invalid!"); + expect(messageText).toContain('Your username is invalid!'); }); - it("should clear form fields between attempts", async () => { - await session.page.type("#username", "test_user"); - await session.page.type("#password", "test_pass"); + it('should clear form fields between attempts', async () => { + await session.page.type('#username', 'test_user'); + await session.page.type('#password', 'test_pass'); // Clear fields await session.page.$eval( - "#username", - (el) => ((el as HTMLInputElement).value = ""), + '#username', + (el) => ((el as HTMLInputElement).value = ''), ); await session.page.$eval( - "#password", - (el) => ((el as HTMLInputElement).value = ""), + '#password', + (el) => ((el as HTMLInputElement).value = ''), ); // Verify fields are empty const username = await session.page.$eval( - "#username", + '#username', (el) => (el as HTMLInputElement).value, ); const password = await session.page.$eval( - "#password", + '#password', (el) => (el as HTMLInputElement).value, ); - expect(username).toBe(""); - expect(password).toBe(""); + expect(username).toBe(''); + expect(password).toBe(''); }); - it("should maintain form state after page refresh", async () => { - const testUsername = "persistence_test"; - await session.page.type("#username", testUsername); + it('should maintain form state after page refresh', async () => { + const testUsername = 'persistence_test'; + await session.page.type('#username', testUsername); await session.page.reload(); // Form should be cleared after refresh const username = await session.page.$eval( - "#username", + '#username', (el) => (el as HTMLInputElement).value, ); - expect(username).toBe(""); + expect(username).toBe(''); }); - describe("Content Extraction", () => { - it("should extract form labels and placeholders", async () => { + describe('Content Extraction', () => { + it('should extract form labels and placeholders', async () => { const usernameLabel = await session.page.$eval( 'label[for="username"]', (el) => el.textContent, ); - expect(usernameLabel).toBe("Username"); + expect(usernameLabel).toBe('Username'); const passwordPlaceholder = await session.page.$eval( - "#password", + '#password', (el) => (el as HTMLInputElement).placeholder, ); - expect(passwordPlaceholder).toBe(""); + expect(passwordPlaceholder).toBe(''); }); }); }); diff --git a/packages/agent/src/tools/browser/navigation.test.ts b/packages/agent/src/tools/browser/navigation.test.ts index 9690a5f..93c41c5 100644 --- a/packages/agent/src/tools/browser/navigation.test.ts +++ b/packages/agent/src/tools/browser/navigation.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { BrowserManager } from "./BrowserManager.js"; -import { BrowserSession } from "./types.js"; +import { BrowserManager } from './BrowserManager.js'; +import { BrowserSession } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); -describe("Browser Navigation Tests", () => { +describe('Browser Navigation Tests', () => { let browserManager: BrowserManager; let session: BrowserSession; - const baseUrl = "https://the-internet.herokuapp.com"; + const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { browserManager = new BrowserManager(); @@ -20,47 +20,47 @@ describe("Browser Navigation Tests", () => { await browserManager.closeAllSessions(); }); - it("should navigate to main page and verify content", async () => { + it('should navigate to main page and verify content', async () => { await session.page.goto(baseUrl); const title = await session.page.title(); - expect(title).toBe("The Internet"); + expect(title).toBe('The Internet'); const headerText = await session.page.$eval( - "h1.heading", + 'h1.heading', (el) => el.textContent, ); - expect(headerText).toBe("Welcome to the-internet"); + expect(headerText).toBe('Welcome to the-internet'); }); - it("should navigate to login page and verify title", async () => { + it('should navigate to login page and verify title', async () => { await session.page.goto(`${baseUrl}/login`); const title = await session.page.title(); - expect(title).toBe("The Internet"); + expect(title).toBe('The Internet'); - const headerText = await session.page.$eval("h2", (el) => el.textContent); - expect(headerText).toBe("Login Page"); + const headerText = await session.page.$eval('h2', (el) => el.textContent); + expect(headerText).toBe('Login Page'); }); - it("should handle 404 pages appropriately", async () => { + it('should handle 404 pages appropriately', async () => { await session.page.goto(`${baseUrl}/nonexistent`); // Wait for the page to stabilize - await session.page.waitForLoadState("networkidle"); + await session.page.waitForLoadState('networkidle'); // Check for 404 content instead of title since title may vary - const bodyText = await session.page.$eval("body", (el) => el.textContent); - expect(bodyText).toContain("Not Found"); + const bodyText = await session.page.$eval('body', (el) => el.textContent); + expect(bodyText).toContain('Not Found'); }); - it("should handle navigation timeouts", async () => { + it('should handle navigation timeouts', async () => { await expect( session.page.goto(`${baseUrl}/slow`, { timeout: 1 }), ).rejects.toThrow(); }); - it("should wait for network idle", async () => { + it('should wait for network idle', async () => { await session.page.goto(baseUrl, { - waitUntil: "networkidle", + waitUntil: 'networkidle', }); expect(session.page.url()).toBe(`${baseUrl}/`); }); diff --git a/packages/agent/src/tools/browser/types.ts b/packages/agent/src/tools/browser/types.ts index c1b4379..22ab62c 100644 --- a/packages/agent/src/tools/browser/types.ts +++ b/packages/agent/src/tools/browser/types.ts @@ -1,4 +1,4 @@ -import type { Browser, Page } from "@playwright/test"; +import type { Browser, Page } from '@playwright/test'; // Browser configuration export interface BrowserConfig { @@ -15,14 +15,14 @@ export interface BrowserSession { // Browser error codes export enum BrowserErrorCode { - LAUNCH_FAILED = "LAUNCH_FAILED", - NAVIGATION_FAILED = "NAVIGATION_FAILED", - SESSION_ERROR = "SESSION_ERROR", - SELECTOR_ERROR = "SELECTOR_ERROR", - TIMEOUT = "TIMEOUT", - UNKNOWN = "UNKNOWN", - SELECTOR_INVALID = "SELECTOR_INVALID", - ELEMENT_NOT_FOUND = "ELEMENT_NOT_FOUND", + LAUNCH_FAILED = 'LAUNCH_FAILED', + NAVIGATION_FAILED = 'NAVIGATION_FAILED', + SESSION_ERROR = 'SESSION_ERROR', + SELECTOR_ERROR = 'SELECTOR_ERROR', + TIMEOUT = 'TIMEOUT', + UNKNOWN = 'UNKNOWN', + SELECTOR_INVALID = 'SELECTOR_INVALID', + ELEMENT_NOT_FOUND = 'ELEMENT_NOT_FOUND', } // Browser error class @@ -33,17 +33,17 @@ export class BrowserError extends Error { public cause?: unknown, ) { super(message); - this.name = "BrowserError"; + this.name = 'BrowserError'; } } // Selector types for element interaction export enum SelectorType { - CSS = "css", - XPATH = "xpath", - TEXT = "text", - ROLE = "role", - TESTID = "testid", + CSS = 'css', + XPATH = 'xpath', + TEXT = 'text', + ROLE = 'role', + TESTID = 'testid', } // Selector options @@ -58,14 +58,14 @@ export const browserSessions: Map = new Map(); // Browser action types export type BrowserAction = - | { type: "goto"; url: string } - | { type: "click"; selector: string; selectorType?: SelectorType } + | { type: 'goto'; url: string } + | { type: 'click'; selector: string; selectorType?: SelectorType } | { - type: "type"; + type: 'type'; selector: string; text: string; selectorType?: SelectorType; } - | { type: "wait"; selector: string; selectorType?: SelectorType } - | { type: "content" } - | { type: "close" }; + | { type: 'wait'; selector: string; selectorType?: SelectorType } + | { type: 'content' } + | { type: 'close' }; diff --git a/packages/agent/src/tools/browser/wait-behavior.test.ts b/packages/agent/src/tools/browser/wait-behavior.test.ts index c95efd4..0d807ad 100644 --- a/packages/agent/src/tools/browser/wait-behavior.test.ts +++ b/packages/agent/src/tools/browser/wait-behavior.test.ts @@ -6,18 +6,18 @@ import { afterAll, beforeEach, vi, -} from "vitest"; +} from 'vitest'; -import { BrowserManager } from "./BrowserManager.js"; -import { BrowserSession } from "./types.js"; +import { BrowserManager } from './BrowserManager.js'; +import { BrowserSession } from './types.js'; // Set global timeout for all tests in this file vi.setConfig({ testTimeout: 15000 }); -describe("Wait Behavior Tests", () => { +describe('Wait Behavior Tests', () => { let browserManager: BrowserManager; let session: BrowserSession; - const baseUrl = "https://the-internet.herokuapp.com"; + const baseUrl = 'https://the-internet.herokuapp.com'; beforeAll(async () => { browserManager = new BrowserManager(); @@ -28,65 +28,65 @@ describe("Wait Behavior Tests", () => { await browserManager.closeAllSessions(); }); - describe("Dynamic Loading Tests", () => { + describe('Dynamic Loading Tests', () => { beforeEach(async () => { await session.page.goto(`${baseUrl}/dynamic_loading/2`); }); - it("should handle dynamic loading with explicit waits", async () => { - await session.page.click("button"); + it('should handle dynamic loading with explicit waits', async () => { + await session.page.click('button'); // Wait for loading element to appear and then disappear - await session.page.waitForSelector("#loading"); - await session.page.waitForSelector("#loading", { state: "hidden" }); + await session.page.waitForSelector('#loading'); + await session.page.waitForSelector('#loading', { state: 'hidden' }); - const finishElement = await session.page.waitForSelector("#finish"); + const finishElement = await session.page.waitForSelector('#finish'); const finishText = await finishElement?.evaluate((el) => el.textContent); - expect(finishText).toBe("Hello World!"); + expect(finishText).toBe('Hello World!'); }); - it("should timeout on excessive wait times", async () => { - await session.page.click("button"); + it('should timeout on excessive wait times', async () => { + await session.page.click('button'); // Attempt to find a non-existent element with short timeout try { - await session.page.waitForSelector("#nonexistent", { timeout: 1000 }); + await session.page.waitForSelector('#nonexistent', { timeout: 1000 }); expect(true).toBe(false); // Should not reach here } catch (error: any) { - expect(error.message).toContain("Timeout"); + expect(error.message).toContain('Timeout'); } }); }); - describe("Dynamic Controls Tests", () => { + describe('Dynamic Controls Tests', () => { beforeEach(async () => { await session.page.goto(`${baseUrl}/dynamic_controls`); }); - it("should wait for element state changes", async () => { + it('should wait for element state changes', async () => { // Click remove button await session.page.click('button:has-text("Remove")'); // Wait for checkbox to be removed - await session.page.waitForSelector("#checkbox", { state: "hidden" }); + await session.page.waitForSelector('#checkbox', { state: 'hidden' }); // Verify gone message - const message = await session.page.waitForSelector("#message"); + const message = await session.page.waitForSelector('#message'); const messageText = await message?.evaluate((el) => el.textContent); expect(messageText).toContain("It's gone!"); }); - it("should handle multiple sequential dynamic changes", async () => { + it('should handle multiple sequential dynamic changes', async () => { // Remove checkbox await session.page.click('button:has-text("Remove")'); - await session.page.waitForSelector("#checkbox", { state: "hidden" }); + await session.page.waitForSelector('#checkbox', { state: 'hidden' }); // Add checkbox back await session.page.click('button:has-text("Add")'); - await session.page.waitForSelector("#checkbox"); + await session.page.waitForSelector('#checkbox'); // Verify checkbox is present - const checkbox = await session.page.$("#checkbox"); + const checkbox = await session.page.$('#checkbox'); expect(checkbox).toBeTruthy(); }); }); diff --git a/packages/agent/src/tools/getTools.test.ts b/packages/agent/src/tools/getTools.test.ts index 6ab465b..2023b55 100644 --- a/packages/agent/src/tools/getTools.test.ts +++ b/packages/agent/src/tools/getTools.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; -import { getTools } from "./getTools.js"; +import { getTools } from './getTools.js'; -describe("getTools", () => { - it("should return a successful result with tools", () => { +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", () => { + it('should include core tools', () => { const tools = getTools(); const toolNames = tools.map((tool) => tool.name); @@ -17,7 +17,7 @@ describe("getTools", () => { expect(toolNames.length).greaterThan(0); }); - it("should have unique tool names", () => { + it('should have unique tool names', () => { const tools = getTools(); const toolNames = tools.map((tool) => tool.name); const uniqueNames = new Set(toolNames); @@ -25,7 +25,7 @@ describe("getTools", () => { expect(toolNames).toHaveLength(uniqueNames.size); }); - it("should have valid schema for each tool", () => { + it('should have valid schema for each tool', () => { const tools = getTools(); for (const tool of tools) { @@ -39,11 +39,11 @@ describe("getTools", () => { } }); - it("should have executable functions", () => { + 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/packages/agent/src/tools/getTools.ts b/packages/agent/src/tools/getTools.ts index 479f411..1acadf3 100644 --- a/packages/agent/src/tools/getTools.ts +++ b/packages/agent/src/tools/getTools.ts @@ -1,17 +1,17 @@ -import { Tool } from "../core/types.js"; +import { Tool } from '../core/types.js'; -import { browseMessageTool } from "./browser/browseMessage.js"; -import { browseStartTool } from "./browser/browseStart.js"; -import { subAgentTool } from "./interaction/subAgent.js"; -import { userPromptTool } from "./interaction/userPrompt.js"; -import { fetchTool } from "./io/fetch.js"; -import { readFileTool } from "./io/readFile.js"; -import { updateFileTool } from "./io/updateFile.js"; -import { respawnTool } from "./system/respawn.js"; -import { sequenceCompleteTool } from "./system/sequenceComplete.js"; -import { shellMessageTool } from "./system/shellMessage.js"; -import { shellStartTool } from "./system/shellStart.js"; -import { sleepTool } from "./system/sleep.js"; +import { browseMessageTool } from './browser/browseMessage.js'; +import { browseStartTool } from './browser/browseStart.js'; +import { subAgentTool } from './interaction/subAgent.js'; +import { userPromptTool } from './interaction/userPrompt.js'; +import { fetchTool } from './io/fetch.js'; +import { readFileTool } from './io/readFile.js'; +import { updateFileTool } from './io/updateFile.js'; +import { respawnTool } from './system/respawn.js'; +import { sequenceCompleteTool } from './system/sequenceComplete.js'; +import { shellMessageTool } from './system/shellMessage.js'; +import { shellStartTool } from './system/shellStart.js'; +import { sleepTool } from './system/sleep.js'; export function getTools(): Tool[] { return [ diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index 62415dc..191ae78 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -1,9 +1,9 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { toolAgent } from "../../core/toolAgent.js"; -import { Tool } from "../../core/types.js"; -import { getTools } from "../getTools.js"; +import { toolAgent } from '../../core/toolAgent.js'; +import { Tool } from '../../core/types.js'; +import { getTools } from '../getTools.js'; const parameterSchema = z.object({ description: z @@ -12,41 +12,33 @@ const parameterSchema = z.object({ .describe("A brief description of the sub-agent's purpose (max 80 chars)"), goal: z .string() - .describe("The main objective that the sub-agent needs to achieve"), - requirements: z - .array(z.string()) - .optional() - .describe("Specific requirements or constraints that must be met"), - context: z + .describe('The main objective that the sub-agent needs to achieve'), + projectContext: z + .string() + .describe('Context about the problem or environment'), + fileContext: z .object({ workingDirectory: z .string() .optional() - .describe("The directory where the sub-agent should operate"), + .describe('The directory where the sub-agent should operate'), relevantFiles: z - .array(z.string()) - .optional() - .describe("List of files that are relevant to the task"), - projectContext: z .string() .optional() - .describe("Additional context about the project or environment"), + .describe( + 'A list of files, which may include ** or * wildcard characters', + ), }) + .describe( + 'When working with files and directories, it is best to be very specific to avoid sub-agents making incorrect assumptions', + ) .optional(), - successCriteria: z - .array(z.string()) - .optional() - .describe("Specific criteria that indicate successful completion"), - suggestedApproach: z - .string() - .optional() - .describe("Optional guidance on how to approach the task"), }); 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; @@ -55,61 +47,53 @@ type ReturnType = z.infer; // Sub-agent specific configuration const subAgentConfig = { maxIterations: 50, - model: process.env.AGENT_MODEL || "claude-3-opus-20240229", + model: process.env.AGENT_MODEL || 'claude-3-opus-20240229', maxTokens: 4096, temperature: 0.7, 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 { goal, requirements, context, successCriteria, suggestedApproach } = + const { description, goal, projectContext, fileContext } = parameterSchema.parse(params); // Construct a well-structured prompt const prompt = [ + `Description: ${description}`, `Goal: ${goal}`, - requirements?.length - ? `\nRequirements:\n${requirements.map((r) => `- ${r}`).join("\n")}` - : "", - context + `Project Context: ${projectContext}`, + fileContext ? `\nContext:\n${[ - context.workingDirectory - ? `- Working Directory: ${context.workingDirectory}` - : "", - context.relevantFiles?.length - ? `- Relevant Files:\n ${context.relevantFiles.map((f) => `- ${f}`).join("\n ")}` - : "", - context.projectContext - ? `- Project Context: ${context.projectContext}` - : "", + fileContext.workingDirectory + ? `- Working Directory: ${fileContext.workingDirectory}` + : '', + fileContext.relevantFiles + ? `- Relevant Files:\n ${fileContext.relevantFiles}` + : '', ] .filter(Boolean) - .join("\n")}` - : "", - successCriteria?.length - ? `\nSuccess Criteria:\n${successCriteria.map((c) => `- ${c}`).join("\n")}` - : "", - suggestedApproach ? `\nSuggested Approach: ${suggestedApproach}` : "", + .join('\n')}` + : '', ] .filter(Boolean) - .join("\n"); + .join('\n'); - const tools = getTools().filter((tool) => tool.name !== "userPrompt"); + const tools = getTools().filter((tool) => tool.name !== 'userPrompt'); // Update config if timeout is specified const config = { diff --git a/packages/agent/src/tools/interaction/userPrompt.ts b/packages/agent/src/tools/interaction/userPrompt.ts index 4542951..c735b68 100644 --- a/packages/agent/src/tools/interaction/userPrompt.ts +++ b/packages/agent/src/tools/interaction/userPrompt.ts @@ -1,13 +1,13 @@ -import * as readline from "readline"; +import * as readline from 'readline'; -import chalk from "chalk"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import chalk from 'chalk'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; +import { Tool } from '../../core/types.js'; 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"); @@ -16,8 +16,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 }) => { @@ -35,7 +35,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/packages/agent/src/tools/io/fetch.ts b/packages/agent/src/tools/io/fetch.ts index 538589e..68027f8 100644 --- a/packages/agent/src/tools/io/fetch.ts +++ b/packages/agent/src/tools/io/fetch.ts @@ -1,24 +1,24 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; +import { Tool } from '../../core/types.js'; 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 @@ -28,15 +28,15 @@ 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", + name: 'fetch', description: - "Executes HTTP requests using native Node.js fetch API, for using APIs, not for browsing the web.", + 'Executes HTTP requests using native Node.js fetch API, for using APIs, not for browsing the web.', parameters: zodToJsonSchema(parameterSchema), returns: zodToJsonSchema(returnSchema), execute: async ( @@ -48,7 +48,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), ); @@ -59,41 +59,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).toString()}` : ""}`, + `${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}`, ); }, }; diff --git a/packages/agent/src/tools/io/readFile.test.ts b/packages/agent/src/tools/io/readFile.test.ts index 3dd3776..9d8c597 100644 --- a/packages/agent/src/tools/io/readFile.test.ts +++ b/packages/agent/src/tools/io/readFile.test.ts @@ -1,29 +1,29 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; -import { MockLogger } from "../../utils/mockLogger.js"; +import { MockLogger } from '../../utils/mockLogger.js'; -import { readFileTool } from "./readFile.js"; +import { readFileTool } from './readFile.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/packages/agent/src/tools/io/readFile.ts b/packages/agent/src/tools/io/readFile.ts index 0403ae7..d80d6c2 100644 --- a/packages/agent/src/tools/io/readFile.ts +++ b/packages/agent/src/tools/io/readFile.ts @@ -1,32 +1,32 @@ -import * as fs from "fs/promises"; -import * as path from "path"; +import * as fs from 'fs/promises'; +import * as path from 'path'; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; +import { Tool } from '../../core/types.js'; 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, max is 10KB", + 'Maximum size to read, prevents reading arbitrarily large files that blow up the context window, max is 10KB', ), 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({ @@ -45,8 +45,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 ( @@ -80,7 +80,7 @@ export const readFileTool: Tool = { ); return { path: filePath, - content: buffer.toString("utf8", 0, bytesRead), + content: buffer.toString('utf8', 0, bytesRead), size: stats.size, range, }; @@ -91,7 +91,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/packages/agent/src/tools/io/updateFile.test.ts b/packages/agent/src/tools/io/updateFile.test.ts index af5fdc3..34bca74 100644 --- a/packages/agent/src/tools/io/updateFile.test.ts +++ b/packages/agent/src/tools/io/updateFile.test.ts @@ -1,34 +1,34 @@ -import { randomUUID } from "crypto"; -import { mkdtemp } from "fs/promises"; -import { tmpdir } from "os"; -import { join } from "path"; +import { randomUUID } from 'crypto'; +import { mkdtemp } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { MockLogger } from "../../utils/mockLogger.js"; -import { shellExecuteTool } from "../system/shellExecute.js"; +import { MockLogger } from '../../utils/mockLogger.js'; +import { shellExecuteTool } from '../system/shellExecute.js'; -import { readFileTool } from "./readFile.js"; -import { updateFileTool } from "./updateFile.js"; +import { readFileTool } from './readFile.js'; +import { updateFileTool } from './updateFile.js'; const logger = new MockLogger(); -describe("updateFile", () => { +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 @@ -36,29 +36,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`); @@ -67,10 +67,10 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: initialContent, }, - description: "test", + description: 'test', }, { logger }, ); @@ -80,31 +80,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 @@ -112,10 +112,10 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: initialContent, }, - description: "test", + description: 'test', }, { logger }, ); @@ -125,31 +125,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 @@ -157,10 +157,10 @@ describe("updateFile", () => { { path: testPath, operation: { - command: "rewrite", + command: 'rewrite', content: initialContent, }, - description: "test", + description: 'test', }, { logger }, ); @@ -171,41 +171,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/packages/agent/src/tools/io/updateFile.ts b/packages/agent/src/tools/io/updateFile.ts index fa5f8b0..cba36d3 100644 --- a/packages/agent/src/tools/io/updateFile.ts +++ b/packages/agent/src/tools/io/updateFile.ts @@ -1,49 +1,49 @@ -import * as fs from "fs"; -import * as fsPromises from "fs/promises"; -import * as path from "path"; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; +import { Tool } from '../../core/types.js'; -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 ( @@ -60,8 +60,8 @@ export const updateFileTool: Tool = { await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true }); - if (operation.command === "update") { - const content = await fsPromises.readFile(absolutePath, "utf8"); + if (operation.command === 'update') { + const content = await fsPromises.readFile(absolutePath, 'utf8'); const occurrences = content.split(operation.oldStr).length - 1; if (occurrences !== 1) { throw new Error( @@ -71,12 +71,12 @@ export const updateFileTool: Tool = { await fsPromises.writeFile( absolutePath, content.replace(operation.oldStr, operation.newStr), - "utf8", + 'utf8', ); - } else if (operation.command === "append") { - await fsPromises.appendFile(absolutePath, operation.content, "utf8"); + } else if (operation.command === 'append') { + await fsPromises.appendFile(absolutePath, operation.content, 'utf8'); } else { - await fsPromises.writeFile(absolutePath, operation.content, "utf8"); + await fsPromises.writeFile(absolutePath, operation.content, 'utf8'); } logger.verbose(`Operation complete: ${operation.command}`); @@ -85,7 +85,7 @@ export const updateFileTool: Tool = { logParameters: (input, { logger }) => { const isFile = fs.existsSync(input.path); logger.info( - `${isFile ? "Modifying" : "Creating"} "${input.path}", ${input.description}`, + `${isFile ? 'Modifying' : 'Creating'} "${input.path}", ${input.description}`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/respawn.test.ts b/packages/agent/src/tools/system/respawn.test.ts index 8f56631..9599441 100644 --- a/packages/agent/src/tools/system/respawn.test.ts +++ b/packages/agent/src/tools/system/respawn.test.ts @@ -1,22 +1,22 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; -import { Logger } from "../../utils/logger"; +import { Logger } from '../../utils/logger'; -import { respawnTool } from "./respawn"; +import { respawnTool } from './respawn'; -describe("respawnTool", () => { - const mockLogger = new Logger({ name: "test" }); +describe('respawnTool', () => { + const mockLogger = new Logger({ name: 'test' }); - it("should have correct name and description", () => { - expect(respawnTool.name).toBe("respawn"); - expect(respawnTool.description).toContain("Resets the agent context"); + it('should have correct name and description', () => { + expect(respawnTool.name).toBe('respawn'); + expect(respawnTool.description).toContain('Resets the agent context'); }); - it("should execute and return confirmation message", async () => { + it('should execute and return confirmation message', async () => { const result = await respawnTool.execute( - { respawnContext: "new context" }, + { respawnContext: 'new context' }, { logger: mockLogger }, ); - expect(result).toBe("Respawn initiated"); + expect(result).toBe('Respawn initiated'); }); }); diff --git a/packages/agent/src/tools/system/respawn.ts b/packages/agent/src/tools/system/respawn.ts index e9b268a..e0158bd 100644 --- a/packages/agent/src/tools/system/respawn.ts +++ b/packages/agent/src/tools/system/respawn.ts @@ -1,33 +1,33 @@ -import { Tool, ToolContext } from "../../core/types.js"; +import { Tool, ToolContext } from '../../core/types.js'; export interface RespawnInput { respawnContext: string; } export const respawnTool: Tool = { - name: "respawn", + name: 'respawn', description: - "Resets the agent context to just the system prompt and provided context", + 'Resets the agent context to just the system prompt and provided context', parameters: { - type: "object", + type: 'object', properties: { respawnContext: { - type: "string", - description: "The context to keep after respawning", + type: 'string', + description: 'The context to keep after respawning', }, }, - required: ["respawnContext"], + required: ['respawnContext'], additionalProperties: false, }, returns: { - type: "string", - description: "A message indicating that the respawn has been initiated", + type: 'string', + description: 'A message indicating that the respawn has been initiated', }, execute: ( _params: Record, _context: ToolContext, ): Promise => { // This is a special case tool - the actual respawn logic is handled in toolAgent - return Promise.resolve("Respawn initiated"); + return Promise.resolve('Respawn initiated'); }, }; diff --git a/packages/agent/src/tools/system/sequenceComplete.ts b/packages/agent/src/tools/system/sequenceComplete.ts index e05ddc3..5c9073a 100644 --- a/packages/agent/src/tools/system/sequenceComplete.ts +++ b/packages/agent/src/tools/system/sequenceComplete.ts @@ -1,22 +1,22 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; +import { Tool } from '../../core/types.js'; 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: ({ result }) => Promise.resolve(result), diff --git a/packages/agent/src/tools/system/shellExecute.test.ts b/packages/agent/src/tools/system/shellExecute.test.ts index fef94b3..f81dfdc 100644 --- a/packages/agent/src/tools/system/shellExecute.test.ts +++ b/packages/agent/src/tools/system/shellExecute.test.ts @@ -1,25 +1,25 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; -import { MockLogger } from "../../utils/mockLogger.js"; +import { MockLogger } from '../../utils/mockLogger.js'; -import { shellExecuteTool } from "./shellExecute.js"; +import { shellExecuteTool } from './shellExecute.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/packages/agent/src/tools/system/shellExecute.ts b/packages/agent/src/tools/system/shellExecute.ts index 3dacd52..ae93154 100644 --- a/packages/agent/src/tools/system/shellExecute.ts +++ b/packages/agent/src/tools/system/shellExecute.ts @@ -1,26 +1,26 @@ -import { exec, ExecException } from "child_process"; -import { promisify } from "util"; +import { exec, ExecException } from 'child_process'; +import { promisify } from 'util'; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; -import { errorToString } from "../../utils/errorToString.js"; +import { Tool } from '../../core/types.js'; +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 @@ -32,7 +32,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; @@ -44,9 +44,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), @@ -64,7 +64,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()}`); @@ -74,22 +74,22 @@ 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?.trim() ?? "", - stderr: execError.stderr?.trim() ?? "", + stdout: execError.stdout?.trim() ?? '', + stderr: execError.stderr?.trim() ?? '', code: execError.code ?? -1, command, }; @@ -99,8 +99,8 @@ export const shellExecuteTool: Tool = { ); return { error: `Unknown error occurred: ${errorToString(error)}`, - stdout: "", - stderr: "", + stdout: '', + stderr: '', code: -1, command, }; diff --git a/packages/agent/src/tools/system/shellMessage.test.ts b/packages/agent/src/tools/system/shellMessage.test.ts index acb731e..1388908 100644 --- a/packages/agent/src/tools/system/shellMessage.test.ts +++ b/packages/agent/src/tools/system/shellMessage.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { MockLogger } from "../../utils/mockLogger.js"; -import { sleep } from "../../utils/sleep.js"; +import { MockLogger } from '../../utils/mockLogger.js'; +import { sleep } from '../../utils/sleep.js'; -import { shellMessageTool, NodeSignals } from "./shellMessage.js"; -import { processStates, shellStartTool } from "./shellStart.js"; +import { shellMessageTool, NodeSignals } from './shellMessage.js'; +import { processStates, shellStartTool } from './shellStart.js'; const logger = new MockLogger(); @@ -12,14 +12,14 @@ 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'); }; -describe("shellMessageTool", () => { - let testInstanceId = ""; +describe('shellMessageTool', () => { + let testInstanceId = ''; beforeEach(() => { processStates.clear(); @@ -32,12 +32,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 }, @@ -49,22 +49,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 }, ); @@ -73,12 +73,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 }, @@ -92,7 +92,7 @@ describe("shellMessageTool", () => { const result = await shellMessageTool.execute( { instanceId, - description: "Check completion", + description: 'Check completion', }, { logger }, ); @@ -102,12 +102,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 }, @@ -119,7 +119,7 @@ describe("shellMessageTool", () => { { instanceId, signal: NodeSignals.SIGTERM, - description: "Send SIGTERM", + description: 'Send SIGTERM', }, { logger }, ); @@ -130,7 +130,7 @@ describe("shellMessageTool", () => { const result2 = await shellMessageTool.execute( { instanceId, - description: "Check on status", + description: 'Check on status', }, { logger }, ); @@ -139,12 +139,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 }, @@ -157,7 +157,7 @@ describe("shellMessageTool", () => { { instanceId, signal: NodeSignals.SIGTERM, - description: "Send signal to terminated process", + description: 'Send signal to terminated process', }, { logger }, ); @@ -166,12 +166,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 }, @@ -184,7 +184,7 @@ describe("shellMessageTool", () => { { instanceId, signal: NodeSignals.SIGTERM, - description: "Send SIGTERM", + description: 'Send SIGTERM', }, { logger }, ); @@ -195,7 +195,7 @@ describe("shellMessageTool", () => { const checkResult = await shellMessageTool.execute( { instanceId, - description: "Check signal state", + description: 'Check signal state', }, { logger }, ); diff --git a/packages/agent/src/tools/system/shellMessage.ts b/packages/agent/src/tools/system/shellMessage.ts index b2279d9..bf75c7c 100644 --- a/packages/agent/src/tools/system/shellMessage.ts +++ b/packages/agent/src/tools/system/shellMessage.ts @@ -1,60 +1,60 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; -import { sleep } from "../../utils/sleep.js"; +import { Tool } from '../../core/types.js'; +import { sleep } from '../../utils/sleep.js'; -import { processStates } from "./shellStart.js"; +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 @@ -66,16 +66,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), @@ -84,7 +84,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 { @@ -98,8 +98,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)`, @@ -111,7 +111,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`); } @@ -120,14 +120,14 @@ export const shellMessageTool: Tool = { await sleep(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()}`); } @@ -146,8 +146,8 @@ export const shellMessageTool: Tool = { logger.verbose(`Process interaction failed: ${error.message}`); return { - stdout: "", - stderr: "", + stdout: '', + stderr: '', completed: false, error: error.message, }; @@ -156,8 +156,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}`, }; @@ -167,7 +167,7 @@ export const shellMessageTool: Tool = { logParameters: (input, { logger }) => { const processState = processStates.get(input.instanceId); logger.info( - `Interacting with shell command "${processState ? processState.command : ""}", ${input.description}`, + `Interacting with shell command "${processState ? processState.command : ''}", ${input.description}`, ); }, logReturns: () => {}, diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index c48331d..f86370f 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -1,13 +1,13 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { MockLogger } from "../../utils/mockLogger.js"; -import { sleep } from "../../utils/sleep.js"; +import { MockLogger } from '../../utils/mockLogger.js'; +import { sleep } from '../../utils/sleep.js'; -import { processStates, shellStartTool } from "./shellStart.js"; +import { processStates, shellStartTool } from './shellStart.js'; const logger = new MockLogger(); -describe("shellStartTool", () => { +describe('shellStartTool', () => { beforeEach(() => { processStates.clear(); }); @@ -19,63 +19,63 @@ 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 }, ); - 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 }, ); - 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 }, ); - 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 }, @@ -83,39 +83,39 @@ describe("shellStartTool", () => { // 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 }, ); - 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 }, ); - 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(); @@ -123,30 +123,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 sleep(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 }, ); - expect(result.mode).toBe("sync"); + expect(result.mode).toBe('sync'); }); }); diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index 3ade4fd..d13210d 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -1,13 +1,13 @@ -import { spawn } from "child_process"; +import { spawn } from 'child_process'; -import { v4 as uuidv4 } from "uuid"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; -import { errorToString } from "../../utils/errorToString.js"; +import { Tool } from '../../core/types.js'; +import { errorToString } from '../../utils/errorToString.js'; -import type { ChildProcess } from "child_process"; +import type { ChildProcess } from 'child_process'; // Define ProcessState type type ProcessState = { @@ -26,40 +26,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; @@ -68,9 +68,9 @@ 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), @@ -105,35 +105,35 @@ 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}`, ); @@ -147,12 +147,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}` : ''}`, }), }); } @@ -163,19 +163,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: ${errorToString(error)}`); resolve({ - mode: "sync", - stdout: "", - stderr: "", + mode: 'sync', + stdout: '', + stderr: '', exitCode: 1, error: errorToString(error), }); @@ -192,7 +192,7 @@ export const shellStartTool: Tool = { ); }, 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/packages/agent/src/tools/system/sleep.test.ts b/packages/agent/src/tools/system/sleep.test.ts index b951388..b870105 100644 --- a/packages/agent/src/tools/system/sleep.test.ts +++ b/packages/agent/src/tools/system/sleep.test.ts @@ -1,17 +1,17 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { MockLogger } from "../../utils/mockLogger"; +import { MockLogger } from '../../utils/mockLogger'; -import { sleepTool } from "./sleep"; +import { sleepTool } from './sleep'; const logger = new MockLogger(); -describe("sleep tool", () => { +describe('sleep tool', () => { beforeEach(() => { vi.useFakeTimers(); }); - it("should sleep for the specified duration", async () => { + it('should sleep for the specified duration', async () => { const sleepPromise = sleepTool.execute({ seconds: 2 }, { logger }); await vi.advanceTimersByTimeAsync(2000); @@ -20,13 +20,13 @@ describe("sleep tool", () => { expect(result).toEqual({ sleptFor: 2 }); }); - it("should reject negative sleep duration", async () => { + it('should reject negative sleep duration', async () => { await expect( sleepTool.execute({ seconds: -1 }, { logger }), ).rejects.toThrow(); }); - it("should reject sleep duration over 1 hour", async () => { + it('should reject sleep duration over 1 hour', async () => { await expect( sleepTool.execute({ seconds: 3601 }, { logger }), ).rejects.toThrow(); diff --git a/packages/agent/src/tools/system/sleep.ts b/packages/agent/src/tools/system/sleep.ts index 20d98a2..ed89288 100644 --- a/packages/agent/src/tools/system/sleep.ts +++ b/packages/agent/src/tools/system/sleep.ts @@ -1,8 +1,8 @@ -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; -import { Tool } from "../../core/types.js"; -import { sleep } from "../../utils/sleep.js"; +import { Tool } from '../../core/types.js'; +import { sleep } from '../../utils/sleep.js'; const MAX_SLEEP_SECONDS = 3600; // 1 hour @@ -11,17 +11,17 @@ const parametersSchema = z.object({ .number() .min(0) .max(MAX_SLEEP_SECONDS) - .describe("Number of seconds to sleep (max 1 hour)"), + .describe('Number of seconds to sleep (max 1 hour)'), }); const returnsSchema = z.object({ - sleptFor: z.number().describe("Actual number of seconds slept"), + sleptFor: z.number().describe('Actual number of seconds slept'), }); export const sleepTool: Tool = { - name: "sleep", + name: 'sleep', description: - "Pauses execution for the specified number of seconds, useful when waiting for async tools to make progress before checking on them", + 'Pauses execution for the specified number of seconds, useful when waiting for async tools to make progress before checking on them', parameters: zodToJsonSchema(parametersSchema), returns: zodToJsonSchema(returnsSchema), async execute(params) { @@ -37,6 +37,6 @@ export const sleepTool: Tool = { return `sleeping for ${seconds} seconds`; }, logReturns() { - return ""; + return ''; }, }; diff --git a/packages/agent/src/utils/logger.test.ts b/packages/agent/src/utils/logger.test.ts index e66b058..393a5b6 100644 --- a/packages/agent/src/utils/logger.test.ts +++ b/packages/agent/src/utils/logger.test.ts @@ -1,17 +1,17 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { Logger, LogLevel } from "./logger.js"; +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,39 +20,39 @@ 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), ); }); - it("should log verbose messages", () => { + it('should log verbose messages', () => { logger.verbose(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); }); - it("should log info messages", () => { + it('should log info messages', () => { logger.info(testMessage); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); }); - it("should log warning messages", () => { + it('should log warning messages', () => { logger.warn(testMessage); expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining(testMessage), ); }); - it("should log error messages", () => { + it('should log error messages', () => { logger.error(testMessage); expect(consoleSpy.error).toHaveBeenCalledWith( expect.stringContaining(testMessage), @@ -60,26 +60,26 @@ describe("Logger", () => { }); }); - 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), diff --git a/packages/agent/src/utils/logger.ts b/packages/agent/src/utils/logger.ts index c8cd80a..47fa443 100644 --- a/packages/agent/src/utils/logger.ts +++ b/packages/agent/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,17 +73,17 @@ export class Logger { currentParent = currentParent.parent; } - this.prefix = " ".repeat(offsetSpaces); + this.prefix = ' '.repeat(offsetSpaces); } private toStrings(messages: unknown[]) { return messages .map((message) => - typeof message === "object" + typeof message === 'object' ? JSON.stringify(message, null, 2) : String(message), ) - .join(" "); + .join(' '); } private formatMessages(level: LogLevel, messages: unknown[]): string { @@ -95,10 +95,15 @@ export class Logger { this.nesting, ); + let combinedPrefix = `${this.prefix}${prefix}`; + if (combinedPrefix.length > 0) { + combinedPrefix += ' '; + } + return formatted - .split("\n") - .map((line) => `${this.prefix}${prefix} ${messageColor(line)}`) - .join("\n"); + .split('\n') + .map((line) => `${combinedPrefix}${messageColor(line)}`) + .join('\n'); } debug(...messages: unknown[]): void { diff --git a/packages/agent/src/utils/mockLogger.ts b/packages/agent/src/utils/mockLogger.ts index efa31ed..e7bda6f 100644 --- a/packages/agent/src/utils/mockLogger.ts +++ b/packages/agent/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/packages/agent/src/utils/stringifyLimited.test.ts b/packages/agent/src/utils/stringifyLimited.test.ts index 605858d..3937b1b 100644 --- a/packages/agent/src/utils/stringifyLimited.test.ts +++ b/packages/agent/src/utils/stringifyLimited.test.ts @@ -1,20 +1,20 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from 'vitest'; -import { stringify2 } from "./stringifyLimited.js"; +import { stringify2 } from './stringifyLimited.js'; -describe("stringify2", () => { - it("should stringify simple objects", () => { - 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", () => { + it('should handle nested objects', () => { const obj = { a: 1, b: { - c: "test", + c: 'test', d: [1, 2, 3], }, }; @@ -24,39 +24,39 @@ describe("stringify2", () => { expect(parsed.b).toBeTruthy(); }); - it("should truncate long values", () => { - 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", () => { + 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", () => { + 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", () => { - 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/packages/agent/src/utils/stringifyLimited.ts b/packages/agent/src/utils/stringifyLimited.ts index a76b500..e058f04 100644 --- a/packages/agent/src/utils/stringifyLimited.ts +++ b/packages/agent/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/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index e51225a..6399391 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # mycoder +## 0.2.0 + +### Minor Changes + +- Make the warning a consent based single show to reduce distractions. Made the initial user prompt green to better conform to the user prompts from the agents being green. + ## 0.1.3 ### Patch Changes diff --git a/packages/cli/bin/cli.js b/packages/cli/bin/cli.js index 6cf1a59..924fff1 100755 --- a/packages/cli/bin/cli.js +++ b/packages/cli/bin/cli.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -import "../dist/index.js"; +import '../dist/index.js'; diff --git a/packages/cli/package.json b/packages/cli/package.json index 9eb48a8..f09f9ea 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "mycoder", "description": "A command line tool using agent that can do arbitrary tasks, including coding tasks", - "version": "0.1.3", + "version": "0.2.0", "type": "module", "bin": "./bin/cli.js", "main": "./dist/index.js", @@ -19,7 +19,7 @@ "url": "https://github.com/drivecore/mycoder-monorepo/issues" }, "scripts": { - "start": "node --no-deprecation dist/index.js", + "start": "node --no-deprecation bin/cli.js", "typecheck": "tsc --noEmit", "build": "tsc", "clean": "rimraf dist", diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index f649d69..d535955 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -1,44 +1,63 @@ -import * as fs from "fs/promises"; -import { createInterface } from "readline/promises"; +import * as fs from 'fs/promises'; +import { createInterface } from 'readline/promises'; +import chalk from 'chalk'; import { toolAgent, Logger, getTools, getAnthropicApiKeyError, -} from "mycoder-agent"; +} from 'mycoder-agent'; -import { SharedOptions } from "../options.js"; -import { getPackageInfo } from "../utils/versionCheck.js"; +import { SharedOptions } from '../options.js'; +import { hasUserConsented, saveUserConsent } from '../settings/settings.js'; +import { getPackageInfo } from '../utils/versionCheck.js'; -import type { CommandModule, Argv } from "yargs"; +import type { CommandModule, Argv } from 'yargs'; interface DefaultArgs extends SharedOptions { prompt?: string; } export const command: CommandModule = { - command: "* [prompt]", - describe: "Execute a prompt or start interactive mode", + command: '* [prompt]', + describe: 'Execute a prompt or start interactive mode', builder: (yargs: Argv): Argv => { - return yargs.positional("prompt", { - type: "string", - description: "The prompt to execute", + return yargs.positional('prompt', { + type: 'string', + description: 'The prompt to execute', }) as Argv; }, handler: async (argv) => { - const logger = new Logger({ name: "Default" }); + 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.", - ); + if (!hasUserConsented()) { + const readline = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + logger.warn( + '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.', + 'Do you consent to using this tool at your own risk? (y/N)', + ); + + const answer = (await readline.question('> ')).trim().toLowerCase(); + readline.close(); + + if (answer === 'y' || answer === 'yes') { + saveUserConsent(); + } else { + logger.info('User did not consent. Exiting.'); + process.exit(0); + } + } try { // Early API key check if (!process.env.ANTHROPIC_API_KEY) { @@ -51,7 +70,7 @@ export const command: CommandModule = { // 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}`, @@ -68,10 +87,12 @@ export const command: CommandModule = { }); try { - logger.info( - "Type your request below or 'help' for usage information. Use Ctrl+C to exit.", + console.log( + chalk.green( + "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(); } @@ -82,27 +103,27 @@ export const command: CommandModule = { 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 = 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/packages/cli/src/commands/tools.ts b/packages/cli/src/commands/tools.ts index f445822..7b1e580 100644 --- a/packages/cli/src/commands/tools.ts +++ b/packages/cli/src/commands/tools.ts @@ -1,7 +1,7 @@ -import { getTools } from "mycoder-agent"; +import { getTools } from 'mycoder-agent'; -import type { CommandModule } from "yargs"; -import type { JsonSchema7Type } from "zod-to-json-schema"; +import type { CommandModule } from 'yargs'; +import type { JsonSchema7Type } from 'zod-to-json-schema'; interface ToolsArgs { [key: string]: unknown; @@ -11,14 +11,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) { @@ -37,22 +37,22 @@ function formatSchema(schema: { } export const command: CommandModule = { - command: "tools", - describe: "List all available tools and their capabilities", + command: 'tools', + describe: 'List all available tools and their capabilities', handler: () => { try { 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 { @@ -63,7 +63,7 @@ export const command: CommandModule = { ); // Returns section - console.log("Returns:"); + console.log('Returns:'); if (tool.returns) { console.log( formatSchema( @@ -74,14 +74,14 @@ export const command: CommandModule = { ), ); } 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/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1979962..073c682 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,18 +1,18 @@ -import { createRequire } from "module"; -import { join } from "path"; -import { fileURLToPath } from "url"; +import { createRequire } from 'module'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; -import * as dotenv from "dotenv"; -import { Logger, LogLevel, getTools } from "mycoder-agent"; -import sourceMapSupport from "source-map-support"; -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -import { fileCommands } from "yargs-file-commands"; +import * as dotenv from 'dotenv'; +import { Logger, LogLevel, getTools } from 'mycoder-agent'; +import sourceMapSupport from 'source-map-support'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { fileCommands } from 'yargs-file-commands'; -import { sharedOptions } from "./options.js"; -import { checkForUpdates } from "./utils/versionCheck.js"; +import { sharedOptions } from './options.js'; +import { checkForUpdates } from './utils/versionCheck.js'; -import type { PackageJson } from "type-fest"; +import type { PackageJson } from 'type-fest'; 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,19 +50,19 @@ 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") + .alias('h', 'help') + .alias('V', 'version') .middleware((argv) => { // Set up logger with the specified log level argv.logger = new Logger({ @@ -74,7 +74,7 @@ const main = async () => { .command( await fileCommands({ commandDirs: [commandsDir], - logLevel: "info", + logLevel: 'info', }), ) .strict() diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index ea726a4..8bcb194 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -1,4 +1,4 @@ -import { LogLevel } from "mycoder-agent"; +import { LogLevel } from 'mycoder-agent'; 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/packages/cli/src/settings/settings.ts b/packages/cli/src/settings/settings.ts index fba67f4..b8482e5 100644 --- a/packages/cli/src/settings/settings.ts +++ b/packages/cli/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)) { @@ -10,3 +10,15 @@ export const getSettingsDir = (): string => { } return settingsDir; }; + +const consentFile = path.join(settingsDir, 'consent.json'); + +export const hasUserConsented = (): boolean => { + return fs.existsSync(consentFile); +}; + +export const saveUserConsent = (): void => { + const timestamp = new Date().toISOString(); + const data = JSON.stringify({ timestamp }, null, 2); + fs.writeFileSync(consentFile, data); +}; diff --git a/packages/cli/src/utils/versionCheck.test.ts b/packages/cli/src/utils/versionCheck.test.ts index 76bfc0c..581e6f6 100644 --- a/packages/cli/src/utils/versionCheck.test.ts +++ b/packages/cli/src/utils/versionCheck.test.ts @@ -1,56 +1,56 @@ -import * as fs from "fs"; -import * as fsPromises from "fs/promises"; -import * as path from "path"; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import * as path from 'path'; -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"; +} from './versionCheck.js'; // Mock the settings module -vi.mock("../settings/settings.js", () => ({ +vi.mock('../settings/settings.js', () => ({ getSettingsDir: vi.fn(), })); // Import after mocking // eslint-disable-next-line import/order -import { getSettingsDir } from "../settings/settings.js"; +import { getSettingsDir } from '../settings/settings.js'; -vi.mock("fs"); -vi.mock("fs/promises"); -vi.mock("mycoder-agent", () => ({ +vi.mock('fs'); +vi.mock('fs/promises'); +vi.mock('mycoder-agent', () => ({ Logger: vi.fn().mockImplementation(() => ({ warn: vi.fn(), })), errorToString: vi.fn(), })); -describe("versionCheck", () => { - describe("generateUpgradeMessage", () => { - it("returns null when versions are the same", () => { - expect(generateUpgradeMessage("1.0.0", "1.0.0", "test-package")).toBe( +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( + 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; @@ -63,57 +63,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; @@ -133,10 +133,10 @@ 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 () => { mockFetch.mockResolvedValueOnce({ ok: true, - json: () => Promise.resolve({ version: "2.0.0" }), + json: () => Promise.resolve({ version: '2.0.0' }), }); const result = await checkForUpdates(); @@ -148,40 +148,40 @@ 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 () => { (fs.existsSync as unknown as ReturnType).mockReturnValue( true, ); ( fsPromises.readFile as unknown as ReturnType - ).mockResolvedValue("2.0.0"); + ).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 () => { (fs.existsSync as unknown as ReturnType).mockReturnValue( true, ); ( fsPromises.readFile as unknown as ReturnType - ).mockRejectedValue(new Error("Test error")); + ).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 () => { (fs.existsSync as unknown as ReturnType).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/packages/cli/src/utils/versionCheck.ts b/packages/cli/src/utils/versionCheck.ts index 2946a51..c4c2b2f 100644 --- a/packages/cli/src/utils/versionCheck.ts +++ b/packages/cli/src/utils/versionCheck.ts @@ -1,26 +1,26 @@ -import * as fs from "fs"; -import * as fsPromises from "fs/promises"; -import { createRequire } from "module"; -import * as path from "path"; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import { createRequire } from 'module'; +import * as path from 'path'; -import chalk from "chalk"; -import { Logger, errorToString } from "mycoder-agent"; -import * as semver from "semver"; +import chalk from 'chalk'; +import { Logger, errorToString } from 'mycoder-agent'; +import * as semver from 'semver'; -import { getSettingsDir } from "../settings/settings.js"; +import { getSettingsDir } from '../settings/settings.js'; -import type { PackageJson } from "type-fest"; +import type { PackageJson } from 'type-fest'; 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 { @@ -39,7 +39,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; } @@ -61,11 +61,11 @@ 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, @@ -76,16 +76,16 @@ export async function checkForUpdates(): Promise { 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:", errorToString(error)); + logger.warn('Error fetching latest version:', errorToString(error)); }); return null; } catch (error) { // Log error but don't throw to handle gracefully - logger.warn("Error checking for updates:", errorToString(error)); + logger.warn('Error checking for updates:', errorToString(error)); return null; } } diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index a2391d0..f6644cc 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -1,20 +1,20 @@ -import { execSync } from "child_process"; +import { execSync } from 'child_process'; -import { expect, test, describe } from "vitest"; +import { expect, test, describe } from 'vitest'; -import { version } from "../package.json"; +import { version } from '../package.json'; -describe("CLI", () => { - test("version command outputs correct version", () => { - const output = execSync("node ./bin/cli.js --version").toString(); +describe('CLI', () => { + test('version command outputs correct version', () => { + const output = execSync('node ./bin/cli.js --version').toString(); expect(output).toContain(version); - expect(output).not.toContain("AI-powered coding assistant"); + expect(output).not.toContain('AI-powered coding assistant'); }); - test("--help command outputs help", () => { - const output = execSync("node ./bin/cli.js --help").toString(); - expect(output).toContain("Commands:"); - expect(output).toContain("Positionals:"); - expect(output).toContain("Options:"); + test('--help command outputs help', () => { + const output = execSync('node ./bin/cli.js --help').toString(); + expect(output).toContain('Commands:'); + expect(output).toContain('Positionals:'); + expect(output).toContain('Options:'); }); }); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee51e9..18ec407 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,2 @@ packages: - - "packages/*" + - 'packages/*'