diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index f7a85a9f..97b4782d 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -43,7 +43,6 @@ export async function POST(req: Request) { } const messages = convertToCoreMessages(body.messages); - console.log('tools:', tools) const result = await streamText({ messages, model: toolContext.model, diff --git a/components/input/InputSettings.tsx b/components/input/InputSettings.tsx index 26e7d33f..d35b7183 100644 --- a/components/input/InputSettings.tsx +++ b/components/input/InputSettings.tsx @@ -70,23 +70,24 @@ export function InputSettings({ className, ...props }: React.ComponentProps<'div } const ourtools = [ - 'create_file', - 'list_repos', - 'rewrite_file', 'scrape_webpage', 'view_file', 'view_hierarchy', - 'create_pull_request', + 'create_file', + 'rewrite_file', + 'delete_file', 'create_branch', - 'fetch_github_issue', + 'create_pull_request', 'update_pull_request', 'close_pull_request', 'list_pull_requests', 'view_pull_request', + 'fetch_github_issue', 'list_open_issues', 'open_issue', 'close_issue', - 'post_github_comment' + 'post_github_comment', + // 'list_repos', ] const buttonClasses = "opacity-75 bg-background text-foreground hover:bg-accent/60 hover:text-accent-foreground rounded px-2 py-1 flex items-center space-x-1 focus:outline-none focus:ring-0 transition-colors duration-200" diff --git a/lib/githubUtils.ts b/lib/githubUtils.ts index 7dacb0df..21bb61d3 100644 --- a/lib/githubUtils.ts +++ b/lib/githubUtils.ts @@ -1,78 +1,128 @@ import { z } from "zod" -async function githubApiRequest(url: string, token: string): Promise { - const response = await fetch(url, { - headers: { - Authorization: `token ${token}`, - Accept: 'application/vnd.github.v3+json', - }, - }); - - if (!response.ok) { - throw new Error(`GitHub API request failed: ${response.statusText}`); - } - - return response.json(); +async function githubApiRequest(url: string, token: string, method: string = 'GET', body?: any): Promise { + const response = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}\n${errorText}`); + } + + return response.json(); } const githubListUserReposArgsSchema = z.object({ - token: z.string(), - perPage: z.number().optional(), - sort: z.enum(['created', 'updated', 'pushed', 'full_name']).optional(), - direction: z.enum(['asc', 'desc']).optional(), + token: z.string(), + perPage: z.number().optional(), + sort: z.enum(['created', 'updated', 'pushed', 'full_name']).optional(), + direction: z.enum(['asc', 'desc']).optional(), }); export async function githubListUserRepos(args: z.infer): Promise { - const { token, perPage, sort, direction } = githubListUserReposArgsSchema.parse(args); - const params = new URLSearchParams(); - - if (perPage !== undefined) params.append('per_page', perPage.toString()); - if (sort !== undefined) params.append('sort', sort); - if (direction !== undefined) params.append('direction', direction); - - const url = `https://api.github.com/user/repos?${params.toString()}`; - - const data = await githubApiRequest(url, token); - - if (!Array.isArray(data)) { - throw new Error("Unexpected data format received from GitHub API"); - } - - return data.map(repo => ({ - name: repo.name, - full_name: repo.full_name, - description: repo.description, - html_url: repo.html_url, - private: repo.private, - updated_at: repo.updated_at, - pushed_at: repo.pushed_at, - })); + console.log("are we here") + const { token, perPage, sort, direction } = githubListUserReposArgsSchema.parse(args); + const params = new URLSearchParams(); + + if (perPage !== undefined) params.append('per_page', perPage.toString()); + if (sort !== undefined) params.append('sort', sort); + if (direction !== undefined) params.append('direction', direction); + + const url = `https://api.github.com/user/repos?${params.toString()}`; + + const data = await githubApiRequest(url, token); + + if (!Array.isArray(data)) { + throw new Error("Unexpected data format received from GitHub API"); + } + + return data.map(repo => ({ + name: repo.name, + full_name: repo.full_name, + description: repo.description, + html_url: repo.html_url, + private: repo.private, + updated_at: repo.updated_at, + pushed_at: repo.pushed_at, + })); } export async function githubReadFile(args: { path: string, token: string, repoOwner: string, repoName: string, branch?: string }): Promise { - const { path, token, repoOwner, repoName, branch } = args; - const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}${branch ? `?ref=${branch}` : ''}`; - - const data = await githubApiRequest(url, token); - - if (data.type !== 'file') { - throw new Error('The path does not point to a file'); - } - - return Buffer.from(data.content, 'base64').toString('utf-8'); + const { path, token, repoOwner, repoName, branch } = args; + const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}${branch ? `?ref=${branch}` : ''}`; + + const data = await githubApiRequest(url, token); + + if (data.type !== 'file') { + throw new Error('The path does not point to a file'); + } + + return Buffer.from(data.content, 'base64').toString('utf-8'); } export async function githubListContents(args: { path: string, token: string, repoOwner: string, repoName: string, branch?: string }): Promise { - const { path, token, repoOwner, repoName, branch } = args; - const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}${branch ? `?ref=${branch}` : ''}`; - - const data = await githubApiRequest(url, token); - - if (!Array.isArray(data)) { - throw new Error('The path does not point to a directory'); - } - - return data.map((item: any) => item.name); + const { path, token, repoOwner, repoName, branch } = args; + const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}${branch ? `?ref=${branch}` : ''}`; + + const data = await githubApiRequest(url, token); + + if (!Array.isArray(data)) { + throw new Error('The path does not point to a directory'); + } + + return data.map((item: any) => item.name); } -// ... (rest of the file remains unchanged) \ No newline at end of file +export async function githubDeleteFile(args: { + path: string, + token: string, + repoOwner: string, + repoName: string, + branch?: string, + message?: string, + committerName?: string, + committerEmail?: string +}): Promise { + const { + path, + token, + repoOwner, + repoName, + branch = 'main', + message, + committerName = "GitHub API", + committerEmail = "noreply@github.com" + } = args; + + // Include the branch in the URL for both GET and DELETE requests + const fileUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}`; + + // First, get the current file to retrieve its SHA + const fileData = await githubApiRequest(`${fileUrl}?ref=${branch}`, token); + + if (!fileData.sha) { + throw new Error(`File not found: ${path} in branch ${branch}`); + } + + // Prepare the request body + const body = { + message: message || `Delete ${path}`, + committer: { + name: committerName, + email: committerEmail + }, + sha: fileData.sha, + branch: branch, + }; + + // Send DELETE request + await githubApiRequest(fileUrl, token, 'DELETE', body); +} diff --git a/lib/systemPrompt.ts b/lib/systemPrompt.ts index a43e07de..52860d91 100644 --- a/lib/systemPrompt.ts +++ b/lib/systemPrompt.ts @@ -4,6 +4,7 @@ import { ToolContext } from "@/types" export function getSystemPrompt(context: ToolContext, selectedTools: string[]): string { const { repo } = context const prompt = repo ? getRepoPrompt(repo, selectedTools) : basePrompt(selectedTools); + console.log("Prompt:", prompt); return prompt } diff --git a/panes/changelog/ChangelogPane.tsx b/panes/changelog/ChangelogPane.tsx index 10ce4f08..2b5e97e9 100644 --- a/panes/changelog/ChangelogPane.tsx +++ b/panes/changelog/ChangelogPane.tsx @@ -10,6 +10,9 @@ const ChangelogPane: React.FC = () => {

Aug 29

  • Added tool to create GitHub issue
  • +
  • Added tool to delete file
  • +
  • Reordered tools, removed list-repo tool
  • +
  • Will warn instead of fail if a needed tool is unchecked

Aug 27

    diff --git a/tools/delete-file.ts b/tools/delete-file.ts new file mode 100644 index 00000000..bc38ea66 --- /dev/null +++ b/tools/delete-file.ts @@ -0,0 +1,73 @@ +import { tool, CoreTool } from 'ai'; +import { z } from 'zod'; +import { githubDeleteFile } from '@/lib/githubUtils'; +import { ToolContext } from '@/types'; + +const params = z.object({ + path: z.string().describe("The path of the file to delete"), + owner: z.string().optional().describe("The owner of the repository"), + repo: z.string().optional().describe("The name of the repository"), + branch: z.string().optional().describe("The branch to delete the file from"), +}); + +type Params = z.infer; + +type Result = { + success: boolean; + error?: string; + summary: string; + details: string; +}; + +export const deleteFileTool = (context: ToolContext): CoreTool => tool({ + description: "Delete file at path", + parameters: params, + execute: async ({ path, owner, repo, branch }: Params): Promise => { + const repoOwner = owner || context.repo?.owner; + const repoName = repo || context.repo?.name; + const repoBranch = branch || context.repo?.branch || 'main'; + + if (!repoOwner || !repoName) { + return { + success: false, + error: "Missing repository information", + summary: "Failed to delete file due to missing repository information", + details: "The repository owner or name is missing. Please provide both in the request or ensure they are set in the context." + }; + } + + if (!context.gitHubToken) { + return { + success: false, + error: "Missing GitHub token", + summary: "Failed to delete file due to missing GitHub token", + details: "The GitHub token is missing. Please ensure it is provided in the context." + }; + } + + try { + await githubDeleteFile({ + path, + token: context.gitHubToken, + repoOwner, + repoName, + branch: repoBranch + }); + + return { + success: true, + summary: `Deleted ${path} in ${repoOwner}/${repoName} on branch ${repoBranch}`, + details: `File ${path} in ${repoOwner}/${repoName} on branch ${repoBranch} has been successfully deleted.` + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(errorMessage); + return { + success: false, + error: errorMessage, + summary: `Failed to delete ${path} in ${repoOwner}/${repoName} on branch ${repoBranch}`, + details: `Failed to delete file at ${path} in ${repoOwner}/${repoName} on branch ${repoBranch}. Error: ${errorMessage}` + }; + } + }, +}); \ No newline at end of file diff --git a/tools/index.ts b/tools/index.ts index 8751f533..89b74ac7 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -10,10 +10,12 @@ import { closePullRequestTool } from "./close-pull-request" import { createBranchTool } from "./create-branch" import { createFileTool } from "./create-file" import { createPullRequestTool } from "./create-pull-request" +import { deleteFileTool } from "./delete-file" import { fetchGitHubIssueTool } from "./fetch-github-issue" import { listOpenIssuesTool } from "./list-open-issues" import { listPullRequestsTool } from "./list-pull-requests" -import { listReposTool } from "./list-repos" +import { openIssueTool } from "./open-issue" +// import { listReposTool } from "./list-repos" import { postGitHubCommentTool } from "./post-github-comment" import { rewriteFileTool } from "./rewrite-file" import { scrapeWebpageTool } from "./scrape-webpage" @@ -21,11 +23,10 @@ import { updatePullRequestTool } from "./update-pull-request" import { viewFileTool } from "./view-file" import { viewHierarchyTool } from "./view-hierarchy" import { viewPullRequestTool } from "./view-pull-request" -import { openIssueTool } from "./open-issue" export const allTools = { create_file: { tool: createFileTool, description: "Create a new file at path with content" }, - list_repos: { tool: listReposTool, description: "List all repositories for the authenticated user" }, + // list_repos: { tool: listReposTool, description: "List all repositories for the authenticated user" }, rewrite_file: { tool: rewriteFileTool, description: "Rewrite file at path with new content" }, scrape_webpage: { tool: scrapeWebpageTool, description: "Scrape webpage for information" }, view_file: { tool: viewFileTool, description: "View file contents at path" }, @@ -40,7 +41,8 @@ export const allTools = { view_pull_request: { tool: viewPullRequestTool, description: "View details of a specific pull request" }, list_open_issues: { tool: listOpenIssuesTool, description: "List all open issues in the repository" }, close_issue: { tool: closeIssueTool, description: "Close an existing GitHub issue" }, - open_issue: { tool: openIssueTool, description: "Open a new GitHub issue" } + open_issue: { tool: openIssueTool, description: "Open a new GitHub issue" }, + delete_file: { tool: deleteFileTool, description: "Delete file at path" } } as const; type ToolName = keyof typeof allTools; @@ -101,4 +103,4 @@ export const getToolContext = async (body: ToolContextBody): Promise; type Result = { - success: boolean; - repos?: any[]; - error?: string; - summary: string; - details: string; + success: boolean; + repos?: any[]; + error?: string; + summary: string; + details: string; }; export const listReposTool = (context: ToolContext): CoreTool => tool({ - // name: 'list_repos', - description: 'Lists the most recent repositories for the authenticated user', - parameters: params, - execute: async ({ perPage, sort, direction }: Params): Promise => { - console.log("Attempting to execute listRepos") - if (!context.user || !context.gitHubToken) { - return { - success: false, - error: "Missing user information or GitHub token", - summary: "Failed to list repositories due to missing context", - details: "The tool context is missing required user information or GitHub token." - }; - } + description: 'Lists the most recent repositories for the authenticated user', + parameters: params, + execute: async ({ perPage, sort, direction }: Params): Promise => { + console.log("Attempting to execute listRepos") + if (!context.user || !context.gitHubToken) { + return { + success: false, + error: "Missing user information or GitHub token", + summary: "Failed to list repositories due to missing context", + details: "The tool context is missing required user information or GitHub token." + }; + } - try { - const repos = await githubListUserRepos({ - token: context.gitHubToken, - ...(perPage !== undefined && { perPage }), - ...(sort !== undefined && { sort }), - ...(direction !== undefined && { direction }) - }); + try { + const repos = await githubListUserRepos({ + token: context.gitHubToken, + ...(perPage !== undefined && { perPage }), + ...(sort !== undefined && { sort }), + ...(direction !== undefined && { direction }) + }); - return { - success: true, - repos, - summary: 'Successfully listed user repositories', - details: `Retrieved ${repos.length} most recent repositories for the authenticated user.` - }; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(errorMessage); - return { - success: false, - error: errorMessage, - summary: 'Failed to list user repositories', - details: `An error occurred while trying to list repositories: ${errorMessage}` - }; - } - }, -}); \ No newline at end of file + return { + success: true, + repos, + summary: 'Successfully listed user repositories', + details: `Retrieved ${repos.length} most recent repositories for the authenticated user.` + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(errorMessage); + return { + success: false, + error: errorMessage, + summary: 'Failed to list user repositories', + details: `An error occurred while trying to list repositories: ${errorMessage}` + }; + } + }, +});