diff --git a/.eslintrc.cjs b/.eslintrc.cjs index fda6101..a5626cd 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,7 +21,25 @@ module.exports = { }, ], 'object-shorthand': ['error', 'always'], - 'simple-import-sort/imports': 'error', + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + // Node.js builtins + ['^node:'], + // External packages + ['^@?\\w'], + // Internal packages and cali-tools + ['^cali-tools'], + // Parent imports + ['^\\.\\.(?!/?$)', '^\\.\\./?$'], + // Other relative imports + ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + // Style imports + ['^.+\\.s?css$'], + ], + }, + ], 'simple-import-sort/exports': 'error', }, overrides: [ diff --git a/bun.lockb b/bun.lockb index 843bde3..7a20dc3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/tools.md b/docs/tools.md deleted file mode 100644 index 8530cbf..0000000 --- a/docs/tools.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tools - -While there are no constraints on how to structure your tools, we follow a few conventions to make it easier for the agent to understand and use them: - -## Explicit names - -Tools should have explicit names that describe what they do. For example, `getAppleSimulators` is better than `listSimulators`. - -## Explicit parameters - -Tools should have explicit parameters that describe what they need to do their job. If tool `A` needs return value of tool `B`, it should use `B` in parameter name. For example, `runAppleBuild` needs `reactNativeConfig` property, so it should use `reactNativeConfig_` as a parameter. - -### Getters - -#### Return Type - -**Success** - -Tools should return an object with information. - -**Error** - -If a tool encounters an error, it should return an object with `error` property set to a string that describes the error. It can also return an `action` if there is a follow-up action that the agent should take to recover. If not specified, the agent will try to decide what to do next. - -### Actions - -#### Return Type - -**Success** - -Tools should return an object with `success` property set to `true` to indicate a successful execution, or a string with a success message. This is useful to indicate additional information that might be helpful for the agent to know. - -You may attach `action` property to the response object to provide a follow-up action that the agent should take. - -**Error** - -Same as for getters. - diff --git a/package.json b/package.json index bc946c2..5ea4704 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "devDependencies": { "@release-it-plugins/workspaces": "^4.2.0", "@release-it/conventional-changelog": "^9.0.3", - "@rslib/core": "^0.1.1", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", "bun-types": "^1.1.33", @@ -15,20 +14,15 @@ "eslint-plugin-simple-import-sort": "^12.0.0", "prettier": "^3.2.5", "release-it": "^17.10.0", + "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.1.3", "vitest": "^2.1.1" }, "license": "MIT", - "patchedDependencies": { - "ai@4.0.3": "patches/ai@4.0.3.patch" - }, "private": true, "scripts": { - "build": "bun run build:tools && bun run build:mcp-server && bun run build:cli", - "build:tools": "cd packages/tools && bun run build", - "build:mcp-server": "cd packages/mcp-server && bun run build", - "build:cli": "cd packages/cali && bun run build", + "build": "bun run --filter '*' build", "release": "release-it" }, "workspaces": [ diff --git a/packages/cali/package.json b/packages/cali/package.json index 541ee19..4a48804 100644 --- a/packages/cali/package.json +++ b/packages/cali/package.json @@ -5,14 +5,15 @@ }, "type": "module", "scripts": { - "build": "rslib build", - "dev": "node --import=tsx ./src/cli.ts", - "start": "node ./dist/index.js" + "prepare": "bun run build", + "clean": "rm -rf dist", + "build": "tsup-node" }, "dependencies": { "@ai-sdk/openai": "^1.0.2", "@clack/prompts": "^0.8.1", "ai": "^4.0.3", + "ai-flows": "^0.1.2", "cali-tools": "0.3.1", "chalk": "^5.3.0", "dedent": "^1.5.3", diff --git a/packages/cali/rslib.config.ts b/packages/cali/rslib.config.ts deleted file mode 100644 index 9639e61..0000000 --- a/packages/cali/rslib.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { defineConfig } from '@rslib/core' - -import { dependencies } from './package.json' - -/** - * We need to bundle `ai` dependency with the CLI, because we have custom patch for it. - * We delete `ai` from dependencies that are passed as `externals`. - */ -// @ts-ignore -delete dependencies.ai - -export default defineConfig({ - lib: [ - { - source: { - entry: { - index: './src/cli.ts', - }, - }, - format: 'esm', - bundle: true, - autoExternal: { - dependencies: false, - }, - output: { - externals: Object.keys(dependencies), - distPath: { - root: 'dist', - }, - }, - }, - ], -}) diff --git a/packages/cali/src/agents.ts b/packages/cali/src/agents.ts new file mode 100644 index 0000000..6424113 --- /dev/null +++ b/packages/cali/src/agents.ts @@ -0,0 +1,175 @@ +import { openai } from '@ai-sdk/openai' +import { confirm, select, text } from '@clack/prompts' +import { tool } from 'ai' +import { agent } from 'ai-flows' +import { z } from 'zod' + +/** + * Tools + */ +import * as androidTools from 'cali-tools/android' +import * as appleTools from 'cali-tools/apple' +import { getReactNativeConfig, startMetroDevServer } from 'cali-tools/react-native' + +/** + * Helper tool to throw errors when something wents wrong on the tool level. + */ +export const somethingWentWrong = tool({ + description: + 'Call this tool when something went wrong and you cannot return what you were asked for', + parameters: z.object({ + error: z + .string() + .describe('Error message with details and potential recovery steps to display to the user'), + }), + execute: async ({ error }): Promise => { + throw new Error(error) + }, +}) + +const askUserFromList = tool({ + description: 'Ask the user to choose one of the provided options', + parameters: z.object({ + question: z.string(), + options: z.array( + z.object({ + value: z.string().describe('The value of the option'), + label: z.string().describe('The label that explains the value'), + }) + ), + }), + execute: async ({ question, options }) => { + const answer = await select({ + message: question, + options, + }) + if (typeof answer !== 'string') { + throw new Error('User cancelled the operation') + } + return answer + }, +}) + +const askUserOpenEndedQuestion = tool({ + description: 'Ask the user to answer a question', + parameters: z.object({ + question: z.string(), + }), + execute: async ({ question }) => { + const answer = await text({ + message: question, + }) + if (typeof answer !== 'string') { + throw new Error('User cancelled the operation') + } + return answer + }, +}) + +const confirmWithUser = tool({ + description: 'Ask the user to confirm something (yes/no)', + parameters: z.object({ + question: z.string(), + }), + execute: async ({ question }) => { + const answer = await confirm({ + message: question, + }) + if (typeof answer !== 'boolean') { + throw new Error('User cancelled the operation') + } + return answer + }, +}) + +const userInputTools = { + askUserOpenEndedQuestion, + askUserFromList, + confirmWithUser, +} + +/** + * Agent that ask the user for input. + */ +export const userInputAgent = agent({ + system: ` + Your job is to ask the user for input and return his answer as string. + You choose the right tool to ask the user for input, depending on the type of question. + If you are given multiple questions, you must ask them one by one, and return all answers. + You must return only user answers, without any unnecessary commentary. + You must include all the details in the answer. + `, + model: openai('gpt-4o'), + tools: userInputTools, +}) + +export const reactNativeAgent = agent({ + system: ` + You are a helpful assistant that helps with everything related to React Native. + + You do not know what platforms are available. + You must run a tool to list available platforms. + + If platform is already specified in context, you must use it. + If platform is not specified in context, you must ask the user to choose one and include chosen platform in the final response. + Use that platform for all operations. + `, + model: openai('gpt-4o'), + tools: { + getReactNativeConfig, + startMetroDevServer, + ...userInputTools, + }, +}) + +export const appleAgent = agent({ + system: ` + You are a helpful assistant that helps with everything related to iOS. + + When running an app on simulator, you must first check if the simulator is running. + If it is not, you must start it. + + If there are multiple simulators or devices available, you must ask the user to choose one first. + Do not ask the user if information is already provided in context. + `, + model: openai('gpt-4o'), + tools: { + ...appleTools, + ...userInputTools, + }, +}) + +export const androidAgent = agent({ + system: ` + You are a helpful assistant that helps with everything related to Android. + + When running an app on emulator, you must first check if the emulator is running. + If it is not, you must start it. + + If there are multiple emulators or devices available, you must ask the user to choose one first. + Do not ask the user if information is already provided in context. + `, + model: openai('gpt-4o'), + tools: { + ...androidTools, + ...userInputTools, + }, +}) + +export const processAgent = agent({ + system: ` + You are a helpful assistant that helps with everything related to process. + You are given a command to execute. + You must execute the command and return the result. + `, + model: openai('gpt-4o'), + tools: { + exit: tool({ + description: 'Exit the application', + parameters: z.object({}), + execute: () => { + process.exit(0) + }, + }), + }, +}) diff --git a/packages/cali/src/cli.ts b/packages/cali/src/cli.ts index f164c63..d7c4109 100755 --- a/packages/cali/src/cli.ts +++ b/packages/cali/src/cli.ts @@ -1,24 +1,21 @@ #!/usr/bin/env node -import 'dotenv/config' - -import { createOpenAI } from '@ai-sdk/openai' -import { log, outro, select, spinner, text } from '@clack/prompts' -import { CoreMessage, generateText } from 'ai' -import * as tools from 'cali-tools' +import { log } from '@clack/prompts' +import { execute } from 'ai-flows' import chalk from 'chalk' import dedent from 'dedent' import { retro } from 'gradient-string' -import { z } from 'zod' -import { systemPrompt } from './prompt.js' -import { getApiKey } from './utils.js' +import { + androidAgent, + appleAgent, + processAgent, + reactNativeAgent, + userInputAgent, +} from './agents.js' +import { mainFlow } from './flows.js' -const MessageSchema = z.union([ - z.object({ type: z.literal('select'), content: z.string(), options: z.array(z.string()) }), - z.object({ type: z.literal('question'), content: z.string() }), - z.object({ type: z.literal('end'), content: z.string() }), -]) +import 'dotenv/config' console.clear() @@ -48,138 +45,27 @@ console.log( console.log() -const AI_MODEL = process.env.AI_MODEL || 'gpt-4o' - -const openai = createOpenAI({ - apiKey: await getApiKey('OpenAI', 'OPENAI_API_KEY'), -}) - -async function startSession(): Promise { - const question = await text({ - message: 'What do you want to do today?', - placeholder: 'e.g. "Build the app" or "See available simulators"', - validate: (value) => (value.length > 0 ? undefined : 'Please provide a valid answer.'), - }) - - if (typeof question === 'symbol') { - outro(chalk.gray('Bye!')) - process.exit(0) - } - - return [ - { - role: 'system', - content: 'What do you want to do today?', - }, - { - role: 'user', - content: question, +try { + const response = await execute(mainFlow, { + agents: { + appleAgent, + androidAgent, + reactNativeAgent, + userInputAgent, + processAgent, }, - ] -} - -let messages = await startSession() - -const s = spinner() - -// eslint-disable-next-line no-constant-condition -while (true) { - s.start(chalk.gray('Thinking...')) - - const response = await generateText({ - model: openai(AI_MODEL), - system: systemPrompt, - tools, - maxSteps: 10, - messages, - onStepStart(toolCalls) { - if (toolCalls.length > 0) { - const message = `Executing: ${chalk.gray(toolCalls.map((toolCall) => toolCall.toolName).join(', '))}` - - let spinner = s.message - for (const toolCall of toolCalls) { - /** - * Certain tools call external helpers outside of our control that pipe output to our stdout. - * In such case, we stop the spinner to avoid glitches and display the output instead. - */ - if ( - [ - 'buildAndroidApp', - 'launchAndroidAppOnDevice', - 'installNpmPackage', - 'uninstallNpmPackage', - ].includes(toolCall.toolName) - ) { - spinner = s.stop - break - } - } - - spinner(message) + onFlowStart(flow) { + if (flow.name) { + log.info(chalk.gray(flow.name)) } }, - }) - - const toolCalls = response.steps.flatMap((step) => - step.toolCalls.map((toolCall) => toolCall.toolName) - ) - - if (toolCalls.length > 0) { - s.stop(`Tools called: ${chalk.gray(toolCalls.join(', '))}`) - } else { - s.stop(chalk.gray('Done.')) - } - - for (const step of response.steps) { - if (step.text.length > 0) { - messages.push({ role: 'assistant', content: step.text }) - } - if (step.toolCalls.length > 0) { - messages.push({ role: 'assistant', content: step.toolCalls }) - } - if (step.toolResults.length > 0) { - // tbd: fix this upstream. for some reason, the tool does not include the type, - // against the spec. - for (const toolResult of step.toolResults) { - if (!toolResult.type) { - toolResult.type = 'tool-result' - } + onFlowFinish(flow) { + if (flow.name) { + log.success(chalk.gray(flow.name)) } - messages.push({ role: 'tool', content: step.toolResults }) - } - } - - // tbd: handle parsing errors - const data = MessageSchema.parse(JSON.parse(response.text)) - - const answer = await (() => { - switch (data.type) { - case 'select': - return select({ - message: data.content, - options: data.options.map((option) => ({ value: option, label: option })), - }) - case 'question': - return text({ - message: data.content, - validate: (value) => (value.length > 0 ? undefined : 'Please provide a valid answer.'), - }) - case 'end': - log.info(data.content) - return text({ - message: 'What do you want to do next?', - validate: (value) => (value.length > 0 ? undefined : 'Please provide a valid answer.'), - }) - } - })() - - if (typeof answer !== 'string') { - messages = await startSession() - continue - } - - messages.push({ - role: 'user', - content: answer as string, + }, }) + log.success(response) +} catch (error) { + log.error(String(error)) } diff --git a/packages/cali/src/flows.ts b/packages/cali/src/flows.ts new file mode 100644 index 0000000..5d4f310 --- /dev/null +++ b/packages/cali/src/flows.ts @@ -0,0 +1,72 @@ +/** + * This flow is used to run the application on the selected platform. + */ +export const runApplicationFlow = { + agent: 'sequenceAgent', + input: [ + { + name: 'checkReactNativeEnvironment', + agent: 'reactNativeAgent', + input: ` + Check if we are in the React Native environment and whether everything is set up correctly. + Return React Native config for the project and ask user to choose platform. + `, + }, + { + agent: 'sequenceAgent', + input: [ + { + name: 'prepareReactNativeEnvironment', + agent: 'reactNativeAgent', + input: 'Start Metro development server', + }, + { + agent: 'oneOfAgent', + input: [ + { + name: 'runApplicationOnApple', + agent: 'appleAgent', + input: 'Run the application on the selected Apple platform.', + when: 'User selected to run application on one of the Apple platforms', + }, + { + name: 'runApplicationOnAndroid', + agent: 'androidAgent', + input: 'Run the application on the selected Android platform.', + when: 'User selected to run application on Android platform', + }, + ], + }, + ], + }, + ], +} + +/** + * Main application flow (aka loop) + */ +export const mainFlow = { + agent: 'sequenceAgent', + input: [ + { + agent: 'userInputAgent', + input: ` + Greet the user and ask an open-ended question about what they want to do today. + + Your capabilities are: + - Running and/or building the React Native application for Apple and Android platforms + + When user asks you to explain your capabilities, list them and then ask user to choose what they want to do. + `, + }, + { + agent: 'oneOfAgent', + input: [ + { + ...runApplicationFlow, + when: 'run or build the application on any platform', + }, + ], + }, + ], +} diff --git a/packages/cali/src/prompt.ts b/packages/cali/src/prompt.ts deleted file mode 100644 index 16fe70c..0000000 --- a/packages/cali/src/prompt.ts +++ /dev/null @@ -1,118 +0,0 @@ -import dedent from 'dedent' - -export const systemPrompt = dedent` - ROLE: - You are a development assistant agent with access to various tools for React Native development. - Your purpose is to help developers be more productive by: - - Understanding and executing their natural language requests - - Using available tools effectively to accomplish tasks - - Helping with development, debugging, and maintenance activities - - TOOL PARAMETERS: - - If tools require parameters, ask the user to provide them explicitly. - - If you can get required parameters by running other tools beforehand, you must run the tools instead of asking. - - TOOL RETURN VALUES: - - If tool returns an array, always ask user to select one of the options. - - Never decide for the user. - - REACT NATIVE SPECIFIC: - - You do not know what platforms are available. You must run a tool to list available platforms. - - If user selects "Debug" mode, always start Metro bundler using "startMetro" tool. - - Never build or run for multiple platforms simultaneously. - - WORKFLOW RULES: - - Ask one clear and concise question at a time. - - If you need more information, ask a follow-up question. - - ERROR HANDLING: - - If a tool call returns an error, you must explain the error to the user and ask user if they want to try again: - { - "type": "select", - "content": "", - "options": ["Retry", "Cancel"] - } - - If you have tools to fix the error, ask user to select one of them: - { - "type": "select", - "content": "", - "options": ["", "", ""] - } - - If you do not have tools to fix the error, you must ask user to fix the error manually: - { - "type": "select", - "content": "", - "options": ["I fixed it", "Cancel"] - } - - If user confirms, you must re-run the same tool. - - RESPONSE FORMAT: - - Your response must be a valid JSON object. - - Your response must not contain any other text. - - Your response must start with { and end with }. - - RESPONSE TYPES: - - If user must select an option: - { - "type": "select", - "content": "", - "options": ["", "", ""] - } - - If user must provide an answer: - { - "type": "question", - "content": "" - } - - If user must confirm an action: - { - "type": "select", - "content": "", - "options": ["", ""] - } - - When you finish processing user task, you must answer with: - { - "type": "end", - "content": "" - } - - EXAMPLES: - - If user must select an option: - - - Here are some tasks you can perform: - 1. Option 1 - 2. Option 2 - - - { - "type": "select", - "content": "Here are some tasks you can perform:", - "options": ["Option 1", "Option 2"] - } - - - - If user must provide an answer: - - - Please provide X so I can do Y. - - - { - "type": "question", - "content": "Please provide X so I can do Y." - } - - - - If you can get required parameters by running other tools beforehand, you must run the tools instead of asking: - - - { - "type": "question", - "content": "Please provide adb path so I can run your app on Android." - } - - - Run "getAdbPath" tool and use its result. - - -` diff --git a/packages/cali/tsup.config.ts b/packages/cali/tsup.config.ts new file mode 100644 index 0000000..1e790a4 --- /dev/null +++ b/packages/cali/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['./src/**/*.ts'], + format: ['esm'], + target: 'node22', + splitting: false, + clean: true, + dts: true, +}) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index b44031f..b6e6c33 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -6,9 +6,9 @@ "cali-mcp-server": "./dist/index.js" }, "scripts": { - "build": "rslib build", - "dev": "node --import=tsx ./src/index.ts", - "inspector": "npx @modelcontextprotocol/inspector node --import=tsx ./src/index.ts" + "prepare": "bun run build", + "clean": "rm -rf dist", + "build": "tsup-node" }, "dependencies": { "cali-tools": "0.3.1", diff --git a/packages/mcp-server/rslib.config.ts b/packages/mcp-server/rslib.config.ts deleted file mode 100644 index 149d498..0000000 --- a/packages/mcp-server/rslib.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from '@rslib/core' - -export default defineConfig({ - lib: [ - { - source: { - entry: { - index: './src/index.ts', - }, - }, - format: 'esm', - output: { - distPath: { - root: 'dist', - }, - }, - }, - ], -}) diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index cbbeca3..bc9a4a9 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -3,9 +3,24 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' -import * as tools from 'cali-tools' import { zodToJsonSchema } from 'zod-to-json-schema' +import * as androidTools from 'cali-tools/android' +import * as appleTools from 'cali-tools/apple' +import * as fsTools from 'cali-tools/fs' +import * as gitTools from 'cali-tools/git' +import * as npmTools from 'cali-tools/npm' +import * as reactNativeTools from 'cali-tools/react-native' + +const tools = { + ...androidTools, + ...appleTools, + ...gitTools, + ...reactNativeTools, + ...fsTools, + ...npmTools, +} + const server = new Server( { name: 'cali-mcp-server', diff --git a/packages/mcp-server/tsup.config.ts b/packages/mcp-server/tsup.config.ts new file mode 100644 index 0000000..d2c236e --- /dev/null +++ b/packages/mcp-server/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['./src/**/*.ts'], + format: ['cjs', 'esm'], + target: 'node22', + splitting: false, + clean: true, + dts: true, +}) diff --git a/packages/tools/README.md b/packages/tools/README.md index 4db6ade..043fe26 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -5,8 +5,8 @@ Collection of tools for building AI agents that work with React Native. Exported ## Usage ```ts -// First, import all tools -import * as tools from "cali-tools"; +// First, import the tools you want +import * as reactNativeTools from "cali-tools/react-native"; import { generateText } from "ai"; await generateText({ diff --git a/packages/tools/package.json b/packages/tools/package.json index 0df3a4e..2022c5e 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -3,21 +3,16 @@ "description": "Tools to build your own AI agents for application development.", "type": "module", "exports": { - ".": { - "source": "./src/index.ts", - "types": "./dist/types/index.d.ts", - "require": "./dist/index.cjs", - "import": "./dist/index.js" + "./*": { + "source": "./src/*.ts", + "types": { + "import": "./dist/index.d.ts", + "require": "./dist/index.d.cts" + }, + "require": "./dist/*.cjs", + "import": "./dist/*.js" } }, - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/types/index.d.ts", - "scripts": { - "build": "bun run build:lib && bun run build:types", - "build:lib": "rslib build", - "build:types": "tsc --emitDeclarationOnly --declaration --outdir dist/types" - }, "dependencies": { "@ai-sdk/openai": "^1.0.2", "@clack/prompts": "^0.8.1", @@ -58,5 +53,10 @@ "version": "0.3.1", "engines": { "node": ">=22" + }, + "scripts": { + "prepare": "bun run build", + "clean": "rm -rf dist", + "build": "tsup-node" } } diff --git a/packages/tools/rslib.config.ts b/packages/tools/rslib.config.ts deleted file mode 100644 index 149cb45..0000000 --- a/packages/tools/rslib.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineConfig } from '@rslib/core' - -export default defineConfig({ - lib: [ - { - source: { - entry: { - index: './src/index.ts', - }, - }, - format: 'esm', - output: { - externals: [/react-native-community/], - distPath: { - root: 'dist', - }, - }, - }, - { - source: { - entry: { - index: './src/index.ts', - }, - }, - format: 'cjs', - output: { - externals: [/react-native-community/], - distPath: { - root: 'dist', - }, - }, - }, - ], -}) diff --git a/packages/tools/src/android.ts b/packages/tools/src/android.ts index 4eec05f..088ea14 100644 --- a/packages/tools/src/android.ts +++ b/packages/tools/src/android.ts @@ -27,7 +27,7 @@ export const getAndroidDevices = tool({ description: dedent` Gets available Android devices and emulators. - Returns an array of devices: + Returns an array of devices with the following properties: - "id" - device ID - "name" - device name - "type" - device type ("device" or "emulator") @@ -67,16 +67,9 @@ export const bootAndroidEmulator = tool({ androidDevice_name: z.string(), }), execute: async ({ adbPath, androidDevice_name: emulatorName }) => { - try { - await tryLaunchEmulator(adbPath, emulatorName) - return { - success: 'Device booted successfully.', - action: `Re-run "getAndroidDevices" to verify ${emulatorName} is in the list, with "booted" set to true.`, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to boot emulator', - } + await tryLaunchEmulator(adbPath, emulatorName) + return { + success: 'Device booted successfully.', } }, }) @@ -105,15 +98,9 @@ export const buildAndroidApp = tool({ // tbd: additional CLI flags, such as activeArchOnly - try { - build(gradleArgs, sourceDir) - return { - success: true, - } - } catch (error) { - return { - error: JSON.stringify(error), - } + build(gradleArgs, sourceDir) + return { + success: true, } }, }) @@ -125,18 +112,9 @@ export const runAdbReverse = tool({ port: z.number(), }), execute: async ({ androidDevice_id: deviceId, port }) => { - try { - tryRunAdbReverse(port, deviceId) - return { - success: true, - } - } catch (error) { - return { - error: - error instanceof Error - ? error.message - : 'Failed to run "adb reverse". Port is not forwared.', - } + tryRunAdbReverse(port, deviceId) + return { + success: true, } }, }) @@ -160,24 +138,16 @@ export const launchAndroidAppOnDevice = tool({ didForwardMetroPortToDevice, }) => { if (!didForwardMetroPortToDevice) { - return { - error: 'Port is not forwarded to device.', - action: 'Run "runAdbReverse" to forward port to device and try again.', - } + throw new Error('Port is not forwarded to device.') } - try { - // @ts-ignore - tryLaunchAppOnDevice(deviceId, { packageName, mainActivity, applicationId }, adbPath, { - appId: '', - appIdSuffix: '', - }) - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to launch app', - } + + // @ts-ignore + tryLaunchAppOnDevice(deviceId, { packageName, mainActivity, applicationId }, adbPath, { + appId: '', + appIdSuffix: '', + }) + return { + success: true, } }, }) diff --git a/packages/tools/src/apple.ts b/packages/tools/src/apple.ts index c772c49..854253d 100644 --- a/packages/tools/src/apple.ts +++ b/packages/tools/src/apple.ts @@ -42,16 +42,9 @@ export const bootAppleSimulator = tool({ deviceId: z.string(), }), execute: async ({ deviceId }) => { - try { - execSync(`xcrun simctl boot ${deviceId}`, { stdio: 'inherit' }) - return { - success: `Device ${deviceId} booted successfully.`, - } - } catch (error) { - return { - error: - error instanceof Error ? error.message : `Failed to boot simulator with ID ${deviceId}`, - } + execSync(`xcrun simctl boot ${deviceId}`, { stdio: 'inherit' }) + return { + success: `Device ${deviceId} booted successfully.`, } }, }) @@ -76,15 +69,10 @@ export const buildAppleAppWithoutStarting = tool({ execute: async ({ platform, ...params }) => { const config = await loadReactNativeConfig() const build = createAppleBuild({ platformName: platform }) - try { - await build([], config, params) - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to build application', - } + + await build([], config, params) + return { + success: true, } }, }) @@ -106,16 +94,10 @@ export const buildStartAppleApp = tool({ execute: async ({ platform, ...params }) => { const run = createAppleRun({ platformName: platform }) - try { - const config = await loadReactNativeConfig() - await run([], config, params) - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to start application', - } + const config = await loadReactNativeConfig() + await run([], config, params) + return { + success: true, } }, }) @@ -128,43 +110,37 @@ export const installPods = tool({ newArchitecture: z.boolean().optional(), }), execute: async ({ newArchitecture, platform, clean }) => { - try { - const config = await loadReactNativeConfig() - const directory = config.project?.[platform]?.sourceDir ?? 'ios' - - if (!directory) { - return { - error: 'Project directory not found', - } - } + const config = await loadReactNativeConfig() + const directory = config.project?.[platform]?.sourceDir ?? 'ios' - if (clean) { - execSync('rm -rf Pods Podfile.lock build', { - cwd: directory, - stdio: 'inherit', - }) + if (!directory) { + return { + error: 'Project directory not found', } + } - const commands = ['bundle exec pod install'] - - for (const command of commands) { - execSync(command, { - cwd: directory, - stdio: 'inherit', - env: { - ...process.env, - ...(newArchitecture ? { RCT_NEW_ARCH_ENABLED: '1' } : {}), - }, - }) - } + if (clean) { + execSync('rm -rf Pods Podfile.lock build', { + cwd: directory, + stdio: 'inherit', + }) + } - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to install pods', - } + const commands = ['bundle exec pod install'] + + for (const command of commands) { + execSync(command, { + cwd: directory, + stdio: 'inherit', + env: { + ...process.env, + ...(newArchitecture ? { RCT_NEW_ARCH_ENABLED: '1' } : {}), + }, + }) + } + + return { + success: true, } }, }) @@ -176,17 +152,11 @@ export const startAppleLogging = tool({ interactive: z.boolean().optional().default(true), }), execute: async ({ platform, ...params }) => { - try { - const config = await loadReactNativeConfig() - const log = createLogCommand({ platformName: platform }) - await log([], config, params) - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to start logging', - } + const config = await loadReactNativeConfig() + const log = createLogCommand({ platformName: platform }) + await log([], config, params) + return { + success: true, } }, }) diff --git a/packages/tools/src/git.ts b/packages/tools/src/git.ts index 4a17098..e6195a6 100644 --- a/packages/tools/src/git.ts +++ b/packages/tools/src/git.ts @@ -11,23 +11,17 @@ export const applyDiff = tool({ diff: z.string(), }), execute: async ({ filePath, diff }) => { - try { - const originalContent = fs.readFileSync(filePath, 'utf8') - const patchedContent = applyPatch(originalContent, diff) + const originalContent = fs.readFileSync(filePath, 'utf8') + const patchedContent = applyPatch(originalContent, diff) - if (patchedContent === false) { - throw new Error('Failed to apply patch - patch may be invalid or not applicable') - } + if (patchedContent === false) { + throw new Error('Failed to apply patch - patch may be invalid or not applicable') + } - fs.writeFileSync(filePath, patchedContent, 'utf8') + fs.writeFileSync(filePath, patchedContent, 'utf8') - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to apply diff', - } + return { + success: true, } }, }) diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts deleted file mode 100644 index 183d3da..0000000 --- a/packages/tools/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './android.js' -export * from './apple.js' -export * from './fs.js' -export * from './git.js' -export * from './npm.js' -export * from './react-native.js' diff --git a/packages/tools/src/npm.ts b/packages/tools/src/npm.ts index 518b7e9..e1ebf31 100644 --- a/packages/tools/src/npm.ts +++ b/packages/tools/src/npm.ts @@ -39,19 +39,13 @@ export const unInstallNpmPackage = tool({ packageManager: z.enum(['yarn', 'npm', 'bun']).optional(), }), execute: async ({ packageNames, packageManager }) => { - try { - const params = { - packageManager: packageManager || 'npm', - root: process.cwd(), - } - await uninstall(packageNames, params) - return { - success: true, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to install package', - } + const params = { + packageManager: packageManager || 'npm', + root: process.cwd(), + } + await uninstall(packageNames, params) + return { + success: true, } }, }) diff --git a/packages/tools/src/react-native.ts b/packages/tools/src/react-native.ts index 5c577b3..2c8072d 100644 --- a/packages/tools/src/react-native.ts +++ b/packages/tools/src/react-native.ts @@ -15,7 +15,7 @@ export const startMetroDevServer = tool({ Returns port Metro server started on. `, parameters: z.object({ - port: z.number().default(8081), + port: z.number().nullable(), reactNativeConfig_root: z.string(), reactNativeConfig_reactNativePath: z.string(), }), @@ -24,16 +24,10 @@ export const startMetroDevServer = tool({ reactNativeConfig_root: root, reactNativeConfig_reactNativePath: reactNativePath, }) => { - try { - const { port: newPort } = await findDevServerPort(port, root) - startServerInNewWindow(newPort, root, reactNativePath, getDefaultUserTerminal()) - return { - success: `Metro server started on port ${newPort}.`, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to start Metro bundler', - } + const { port: newPort } = await findDevServerPort(port ?? 8081, root) + startServerInNewWindow(newPort, root, reactNativePath, getDefaultUserTerminal()) + return { + success: `Metro server started on port ${newPort}.`, } }, }) @@ -67,25 +61,19 @@ export const getReactNativeConfig = tool({ `, parameters: z.object({}), execute: async () => { - try { - const { - root, - reactNativePath: path, - reactNativeVersion: version, - project, - platforms, - } = await loadReactNativeConfig() - return { - root, - path, - version, - project, - platforms, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to get React Native config', - } + const { + root, + reactNativePath: path, + reactNativeVersion: version, + project, + platforms, + } = await loadReactNativeConfig() + return { + root, + path, + version, + project, + platforms, } }, }) @@ -106,36 +94,30 @@ export const listReactNativeLibraries = tool({ search: z.string().optional(), }), execute: async ({ search }) => { - try { - const response = await fetch( - `https://reactnative.directory/api/libraries${search ? `?search=${search}` : ''}` - ) - const { libraries } = await response.json() + const response = await fetch( + `https://reactnative.directory/api/libraries${search ? `?search=${search}` : ''}` + ) + const { libraries } = await response.json() - const mappedLibraries = libraries.map((library: any) => ({ - name: `${library.github.name} (★ ${library.github.stats.stars})`, - description: library.description, - npmPackageName: library.npmPkg, - score: library.score, - url: library.github.urls.repo, - })) + const mappedLibraries = libraries.map((library: any) => ({ + name: `${library.github.name} (★ ${library.github.stats.stars})`, + description: library.description, + npmPackageName: library.npmPkg, + score: library.score, + url: library.github.urls.repo, + })) - return { - success: true, - action: dedent` - Ask user to pick a library from the list. - Offer user an option to try different search query. - Offer user an option to cancel the operation and proceed with something else. + return { + success: true, + action: dedent` + Ask user to pick a library from the list. + Offer user an option to try different search query. + Offer user an option to cancel the operation and proceed with something else. - For each library, you can use "installNpmPackage" tool to install it. - You can also offer to display package description or visit Github repository. - `, - libraries: mappedLibraries, - } - } catch (error) { - return { - error: error instanceof Error ? error.message : 'Failed to start Metro bundler', - } + For each library, you can use "installNpmPackage" tool to install it. + You can also offer to display package description or visit Github repository. + `, + libraries: mappedLibraries, } }, }) diff --git a/packages/tools/tsup.config.ts b/packages/tools/tsup.config.ts new file mode 100644 index 0000000..d2c236e --- /dev/null +++ b/packages/tools/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['./src/**/*.ts'], + format: ['cjs', 'esm'], + target: 'node22', + splitting: false, + clean: true, + dts: true, +}) diff --git a/patches/ai@4.0.3.patch b/patches/ai@4.0.3.patch deleted file mode 100644 index 710e283..0000000 --- a/patches/ai@4.0.3.patch +++ /dev/null @@ -1,55 +0,0 @@ -diff --git a/dist/index.d.ts b/dist/index.d.ts -index 6d9d7ffa9c78b51a208a04189d842186a34e78cd..24aede8005b530445fb0788fefe53d98bd9ab2fc 100644 ---- a/dist/index.d.ts -+++ b/dist/index.d.ts -@@ -1558,6 +1558,10 @@ changing the tool call and result types in the result. - */ - experimental_activeTools?: Array; - /** -+ Callback that is called when each step (LLM call) is started -+ */ -+ onStepStart?: (toolCalls: ToolCallArray) => Promise | void; -+ /** - Callback that is called when each step (LLM call) is finished, including intermediate steps. - */ - onStepFinish?: (event: StepResult) => Promise | void; -diff --git a/dist/index.js b/dist/index.js -index f8002b76aae8e7b915b7a16b3c9ff68063e9e78a..dff50cb864846302688f22ea8aa68965601b4144 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -3264,6 +3264,7 @@ async function generateText({ - currentDate = () => /* @__PURE__ */ new Date() - } = {}, - onStepFinish, -+ onStepStart, - ...settings - }) { - if (maxSteps < 1) { -@@ -3424,6 +3425,7 @@ async function generateText({ - currentToolCalls = ((_a11 = currentModelResponse.toolCalls) != null ? _a11 : []).map( - (modelToolCall) => parseToolCall({ toolCall: modelToolCall, tools }) - ); -+ await (onStepStart == null ? void 0 : onStepStart(currentToolCalls)); - currentToolResults = tools == null ? [] : await executeTools({ - toolCalls: currentToolCalls, - tools, -diff --git a/dist/index.mjs b/dist/index.mjs -index 667c98e17072b65f29597277a734127f69fdc83b..586f1082a0c4bf21f04af47031b0d41dc4d5c028 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -3230,6 +3230,7 @@ async function generateText({ - generateId: generateId3 = originalGenerateId3, - currentDate = () => /* @__PURE__ */ new Date() - } = {}, -+ onStepStart, - onStepFinish, - ...settings - }) { -@@ -3391,6 +3392,7 @@ async function generateText({ - currentToolCalls = ((_a11 = currentModelResponse.toolCalls) != null ? _a11 : []).map( - (modelToolCall) => parseToolCall({ toolCall: modelToolCall, tools }) - ); -+ await (onStepStart == null ? void 0 : onStepStart(currentToolCalls)); - currentToolResults = tools == null ? [] : await executeTools({ - toolCalls: currentToolCalls, - tools,