diff --git a/.gitignore b/.gitignore index 12777329698..ddda9110d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ logs # Vite development .vite-port + +pearai-mcp \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5d7799e0150..15d83a004e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "fast-deep-equal": "^3.1.3", "fast-xml-parser": "^4.5.1", "fastest-levenshtein": "^1.0.16", + "form-data": "^4.0.0", "fzf": "^0.5.2", "get-folder-size": "^5.0.0", "i18next": "^24.2.2", @@ -39,6 +40,7 @@ "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-cache": "^5.1.2", + "node-fetch": "^2.7.0", "node-ipc": "^12.0.0", "openai": "^4.78.1", "os-name": "^6.0.0", diff --git a/package.json b/package.json index 1b372e92267..a5f24509e01 100644 --- a/package.json +++ b/package.json @@ -435,7 +435,9 @@ "vscode-material-icons": "^0.1.1", "web-tree-sitter": "^0.22.6", "workerpool": "^9.2.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "form-data": "^4.0.0", + "node-fetch": "^2.7.0" }, "devDependencies": { "@changesets/cli": "^2.27.10", diff --git a/src/core/Cline.ts b/src/core/Cline.ts index a2e876f0007..03c1b1d1e96 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -92,6 +92,7 @@ import { ClineProvider } from "./webview/ClineProvider" import { validateToolUse } from "./mode-validator" import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace" import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence" +import { pearaiDeployWebappTool } from "./tools/pearaiDeployWebappTool" type UserContent = Array @@ -1310,6 +1311,8 @@ export class Cline extends EventEmitter { const modeName = getModeBySlug(mode, customModes)?.name ?? mode return `[${block.name} in ${modeName} mode: '${message}']` } + default: + return `[${block.name}]` } } @@ -1555,6 +1558,16 @@ export class Cline extends EventEmitter { askFinishSubTaskApproval, ) break + case "pearai_deploy_webapp": + await pearaiDeployWebappTool( + this, + block, + askApproval, + handleError, + pushToolResult, + removeClosingTag + ) + break } break diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index d3e75d7b09b..ef3bbc5af63 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -20,6 +20,7 @@ import { getUseMcpToolDescription } from "./use-mcp-tool" import { getAccessMcpResourceDescription } from "./access-mcp-resource" import { getSwitchModeDescription } from "./switch-mode" import { getNewTaskDescription } from "./new-task" +import { getPearaiDeployWebappDescription } from "./pearai-deploy-webapp" // Map of tool names to their description functions const toolDescriptionMap: Record string | undefined> = { @@ -39,6 +40,7 @@ const toolDescriptionMap: Record string | undefined> new_task: (args) => getNewTaskDescription(args), insert_content: (args) => getInsertContentDescription(args), search_and_replace: (args) => getSearchAndReplaceDescription(args), + pearai_deploy_webapp: (args) => getPearaiDeployWebappDescription(args), apply_diff: (args) => args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", } @@ -122,4 +124,6 @@ export { getSwitchModeDescription, getInsertContentDescription, getSearchAndReplaceDescription, + getNewTaskDescription, + getPearaiDeployWebappDescription, } diff --git a/src/core/prompts/tools/pearai-deploy-webapp.ts b/src/core/prompts/tools/pearai-deploy-webapp.ts new file mode 100644 index 00000000000..ca1b55a57b3 --- /dev/null +++ b/src/core/prompts/tools/pearai-deploy-webapp.ts @@ -0,0 +1,54 @@ +import { ToolArgs } from "./types" + +export function getPearaiDeployWebappDescription(args: ToolArgs): string { + return ` +pearai_deploy_webapp + +Deploy a web application to Netlify. This tool can be used for both new deployments and redeployments. + + + +zip_file_path +string +Absoulute path to the zip file containing the web application files +true + + +env_file_path +string +Absoulute path to the environment file containing deployment configuration +true + + +site_id +string +Optional site ID for redeploying an existing site. If not provided, a new site will be created. +false + + +isStatic +boolean +Whether this is a static site deployment (true) or a server-side rendered site (false) +true + + + +To deploy a new web application: + +pearai_deploy_webapp +/path/to/app.zip +/path/to/.env +true + + +To redeploy an existing site: + +pearai_deploy_webapp +/path/to/app.zip +/path/to/.env +your-site-id +true + + +` +} \ No newline at end of file diff --git a/src/core/tools/pearaiDeployWebappTool.ts b/src/core/tools/pearaiDeployWebappTool.ts new file mode 100644 index 00000000000..472da2ee173 --- /dev/null +++ b/src/core/tools/pearaiDeployWebappTool.ts @@ -0,0 +1,151 @@ +import path from "path" +import fs from "fs/promises" +import { Cline } from "../Cline" +import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { formatResponse } from "../prompts/responses" +import { getReadablePath } from "../../utils/path" +import { isPathOutsideWorkspace } from "../../utils/pathUtils" +import { fileExistsAtPath } from "../../utils/fs" +import FormData from "form-data" +import fetch from "node-fetch" +import * as vscode from "vscode" + +const SERVER_URL = "https://server.trypear.ai/pearai-server-api2" + +export async function pearaiDeployWebappTool( + cline: Cline, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +) { + const zip_file_path: string | undefined = block.params.zip_file_path + const env_file_path: string | undefined = block.params.env_file_path + const site_id: string | undefined = block.params.site_id + const isStatic: boolean = block.params.isStatic === "true" + + try { + if (block.partial) { + const partialMessage = JSON.stringify({ + tool: "pearai_deploy_webapp", + zip_file_path: removeClosingTag("zip_file_path", zip_file_path), + env_file_path: removeClosingTag("env_file_path", env_file_path), + site_id: removeClosingTag("site_id", site_id), + isStatic: removeClosingTag("isStatic", isStatic?.toString()), + }) + + await cline.ask("tool", partialMessage, block.partial).catch(() => {}) + return + } + + // Validate required parameters + if (!zip_file_path) { + cline.consecutiveMistakeCount++ + cline.recordToolError("pearai_deploy_webapp") + pushToolResult(await cline.sayAndCreateMissingParamError("pearai_deploy_webapp", "zip_file_path")) + return + } + + if (!env_file_path) { + cline.consecutiveMistakeCount++ + cline.recordToolError("pearai_deploy_webapp") + pushToolResult(await cline.sayAndCreateMissingParamError("pearai_deploy_webapp", "env_file_path")) + return + } + + // Check if files exist + if (!fileExistsAtPath(zip_file_path)) { + cline.recordToolError("pearai_deploy_webapp") + pushToolResult(formatResponse.toolError(`Zip file not found at path: ${getReadablePath(zip_file_path)}`)) + return + } + + if (!fileExistsAtPath(env_file_path)) { + cline.recordToolError("pearai_deploy_webapp") + pushToolResult(formatResponse.toolError(`Environment file not found at path: ${getReadablePath(env_file_path)}`)) + return + } + + cline.consecutiveMistakeCount = 0 + + const toolMessage = JSON.stringify({ + tool: "pearai_deploy_webapp", + zip_file_path, + env_file_path, + site_id, + isStatic, + }) + + const didApprove = await askApproval("tool", toolMessage) + + if (!didApprove) { + return + } + + // Read files + const zipContent = await fs.readFile(zip_file_path) + const envContent = await fs.readFile(env_file_path) + + // Prepare form data + const form = new FormData() + form.append("zip_file", zipContent, { + filename: "dist.zip", + contentType: "application/zip", + }) + form.append("env_file", envContent, { + filename: ".env", + contentType: "text/plain", + }) + if (site_id) { + form.append("site_id", site_id) + } + if (isStatic) { + form.append("static", "true") + } + + // Get auth token from extension context + const authToken = await vscode.commands.executeCommand("pearai-roo-cline.getPearAIApiKey") + if (!authToken) { + vscode.commands.executeCommand("pearai-roo-cline.PearAIKeysNotFound", undefined) + vscode.window.showErrorMessage("PearAI API key not found.", "Login to PearAI").then(async (selection) => { + if (selection === "Login to PearAI") { + const extensionUrl = `${vscode.env.uriScheme}://pearai.pearai/auth` + const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(extensionUrl)) + vscode.env.openExternal( + await vscode.env.asExternalUri( + vscode.Uri.parse(`https://trypear.ai/signin?callback=${callbackUri.toString()}`), + ), + ) + } + }) + throw new Error("PearAI API key not found. Please login to PearAI.") + } + + // Make POST request to deployment endpoint + const endpoint = site_id ? `${SERVER_URL}/redeploy-netlify` : `${SERVER_URL}/deploy-netlify` + const response = await fetch(endpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${authToken}`, + }, + body: form, + }) + + if (!response.ok) { + throw new Error(`Deployment failed with status ${response.status}: ${await response.text()}`) + } + + const result = await response.text() + pushToolResult( + formatResponse.toolResult( + `Successfully deployed webapp${site_id ? ` to site ${site_id}` : " (new site)"}${isStatic ? " (static deployment)" : ""}\n\n${result}` + ) + ) + + return + } catch (error) { + await handleError("deploying webapp", error) + return + } +} \ No newline at end of file diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index d3a05f43ced..5a13ae17aac 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -581,6 +581,7 @@ type RooCodeEvents = { | "switch_mode" | "new_task" | "fetch_instructions" + | "pearai_deploy_webapp" ), string, ] diff --git a/src/exports/types.ts b/src/exports/types.ts index e964fdbe46c..a76c4f8803d 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -590,6 +590,7 @@ type RooCodeEvents = { | "switch_mode" | "new_task" | "fetch_instructions" + | "pearai_deploy_webapp" ), string, ] diff --git a/src/extension.ts b/src/extension.ts index 94085091801..20100744b45 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -182,6 +182,12 @@ export async function activate(context: vscode.ExtensionContext) { }), ) + context.subscriptions.push( + vscode.commands.registerCommand("pearai-roo-cline.getPearAIApiKey", async () => { + return await context.secrets.get("pearaiApiKey") + }), + ) + /* We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes. diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 9b8175053b8..181c0709c8a 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -918,6 +918,7 @@ export const toolNames = [ "switch_mode", "new_task", "fetch_instructions", + "pearai_deploy_webapp", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 1a6eb84ad71..d1722a9b20e 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -63,6 +63,10 @@ export const toolParamNames = [ "ignore_case", "start_line", "end_line", + "zip_file_path", + "env_file_path", + "site_id", + "isStatic", ] as const export type ToolParamName = (typeof toolParamNames)[number] @@ -157,6 +161,12 @@ export interface SearchAndReplaceToolUse extends ToolUse { Partial, "use_regex" | "ignore_case" | "start_line" | "end_line">> } +export interface PearaiDeployWebappToolUse extends ToolUse { + name: "pearai_deploy_webapp" + params: Required, "zip_file_path" | "env_file_path" | "isStatic">> & + Partial, "site_id">> +} + // Define tool group configuration export type ToolGroupConfig = { tools: readonly string[] @@ -181,6 +191,7 @@ export const TOOL_DISPLAY_NAMES: Record = { new_task: "create new task", insert_content: "insert content", search_and_replace: "search and replace", + pearai_deploy_webapp: "deploy webapp", } as const export type { ToolGroup } @@ -197,7 +208,7 @@ export const TOOL_GROUPS: Record = { tools: ["browser_action"], }, command: { - tools: ["execute_command"], + tools: ["execute_command", "pearai_deploy_webapp"], }, mcp: { tools: ["use_mcp_tool", "access_mcp_resource"],