Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ logs

# Vite development
.vite-port

pearai-mcp
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Anthropic.Messages.ContentBlockParam>

Expand Down Expand Up @@ -1310,6 +1311,8 @@ export class Cline extends EventEmitter<ClineEvents> {
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
return `[${block.name} in ${modeName} mode: '${message}']`
}
default:
return `[${block.name}]`
}
}

Expand Down Expand Up @@ -1555,6 +1558,16 @@ export class Cline extends EventEmitter<ClineEvents> {
askFinishSubTaskApproval,
)
break
case "pearai_deploy_webapp":
await pearaiDeployWebappTool(
this,
block,
askApproval,
handleError,
pushToolResult,
removeClosingTag
)
break
}

break
Expand Down
4 changes: 4 additions & 0 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, (args: ToolArgs) => string | undefined> = {
Expand All @@ -39,6 +40,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => 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 }) : "",
}
Expand Down Expand Up @@ -122,4 +124,6 @@ export {
getSwitchModeDescription,
getInsertContentDescription,
getSearchAndReplaceDescription,
getNewTaskDescription,
getPearaiDeployWebappDescription,
}
54 changes: 54 additions & 0 deletions src/core/prompts/tools/pearai-deploy-webapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ToolArgs } from "./types"

export function getPearaiDeployWebappDescription(args: ToolArgs): string {
return `<tool_description>
<tool_name>pearai_deploy_webapp</tool_name>
<description>
Deploy a web application to Netlify. This tool can be used for both new deployments and redeployments.
</description>
<parameters>
<parameter>
<name>zip_file_path</name>
<type>string</type>
<description>Absoulute path to the zip file containing the web application files</description>
<required>true</required>
</parameter>
<parameter>
<name>env_file_path</name>
<type>string</type>
<description>Absoulute path to the environment file containing deployment configuration</description>
<required>true</required>
</parameter>
<parameter>
<name>site_id</name>
<type>string</type>
<description>Optional site ID for redeploying an existing site. If not provided, a new site will be created.</description>
<required>false</required>
</parameter>
<parameter>
<name>isStatic</name>
<type>boolean</type>
<description>Whether this is a static site deployment (true) or a server-side rendered site (false)</description>
<required>true</required>
</parameter>
</parameters>
<usage>
To deploy a new web application:
<example>
<tool>pearai_deploy_webapp</tool>
<parameter name="zip_file_path">/path/to/app.zip</parameter>
<parameter name="env_file_path">/path/to/.env</parameter>
<parameter name="isStatic">true</parameter>
</example>

To redeploy an existing site:
<example>
<tool>pearai_deploy_webapp</tool>
<parameter name="zip_file_path">/path/to/app.zip</parameter>
<parameter name="env_file_path">/path/to/.env</parameter>
<parameter name="site_id">your-site-id</parameter>
<parameter name="isStatic">true</parameter>
</example>
</usage>
</tool_description>`
}
151 changes: 151 additions & 0 deletions src/core/tools/pearaiDeployWebappTool.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ type RooCodeEvents = {
| "switch_mode"
| "new_task"
| "fetch_instructions"
| "pearai_deploy_webapp"
),
string,
]
Expand Down
1 change: 1 addition & 0 deletions src/exports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ type RooCodeEvents = {
| "switch_mode"
| "new_task"
| "fetch_instructions"
| "pearai_deploy_webapp"
),
string,
]
Expand Down
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ export const toolNames = [
"switch_mode",
"new_task",
"fetch_instructions",
"pearai_deploy_webapp",
] as const

export const toolNamesSchema = z.enum(toolNames)
Expand Down
13 changes: 12 additions & 1 deletion src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -157,6 +161,12 @@ export interface SearchAndReplaceToolUse extends ToolUse {
Partial<Pick<Record<ToolParamName, string>, "use_regex" | "ignore_case" | "start_line" | "end_line">>
}

export interface PearaiDeployWebappToolUse extends ToolUse {
name: "pearai_deploy_webapp"
params: Required<Pick<Record<ToolParamName, string>, "zip_file_path" | "env_file_path" | "isStatic">> &
Partial<Pick<Record<ToolParamName, string>, "site_id">>
}

// Define tool group configuration
export type ToolGroupConfig = {
tools: readonly string[]
Expand All @@ -181,6 +191,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
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 }
Expand All @@ -197,7 +208,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
tools: ["browser_action"],
},
command: {
tools: ["execute_command"],
tools: ["execute_command", "pearai_deploy_webapp"],
},
mcp: {
tools: ["use_mcp_tool", "access_mcp_resource"],
Expand Down
Loading