Files
shockbot/entry.js
T
Pullfrog Action 7ef44eb254 try esm action
2025-11-06 19:03:19 -05:00

61 lines
20 KiB
JavaScript
Executable File

#!/usr/bin/env node
import*as Q from"@actions/core";import{type as Ee}from"arktype";import{type as he}from"arktype";import{query as fe}from"@anthropic-ai/claude-agent-sdk";import*as a from"@actions/core";var l=!!process.env.GITHUB_ACTIONS,X=process.env.LOG_LEVEL==="debug";function Z(e){l?a.startGroup(e):console.group(e)}function ee(){l?a.endGroup():console.groupEnd()}function te(e,t){let{title:o,maxWidth:n=80,indent:s="",padding:r=1}=t||{},p=e.trim().split(`
`),c=[];for(let h of p)if(h.length<=n-r*2)c.push(h);else{let k=h.split(" "),b="";for(let v of k){let P=b?`${b} ${v}`:v;P.length<=n-r*2?b=P:b?(c.push(b),b=v):(c.push(v.substring(0,n-r*2)),b=v.substring(n-r*2))}b&&c.push(b)}let u=Math.max(...c.map(h=>h.length)),y=u+r*2,g="";if(o){let h=` ${o} `,k=Math.max(0,y-h.length);g+=`${s}\u250C${h}${"\u2500".repeat(k)}\u2510
`}o||(g+=`${s}\u250C${"\u2500".repeat(y)}\u2510
`);for(let h of c){let k=h.padEnd(u);g+=`${s}\u2502${" ".repeat(r)}${k}${" ".repeat(r)}\u2502
`}return g+=`${s}\u2514${"\u2500".repeat(y)}\u2518`,g}function oe(e,t){let o=te(e,t);a.info(o),l&&a.summary.addRaw(`\`\`\`
${e}
\`\`\`
`)}async function ne(e,t){let{title:o}=t||{},n=e.map(r=>r.map(p=>typeof p=="string"?{data:p}:p));if(l){let r=a.summary;o&&r.addRaw(`**${o}**
`),r.addTable(n)}o&&a.info(`
${o}`);let s=n.map(r=>r.map(p=>p.data).join(" | ")).join(`
`);a.info(`
${s}
`)}function re(e=50){let t="\u2500".repeat(e);a.info(t),l&&a.summary.addRaw(`---
`)}var i={info:e=>{a.info(e),l&&a.summary.addRaw(`${e}
`)},warning:e=>{a.warning(e),l&&a.summary.addRaw(`\u26A0\uFE0F ${e}
`)},error:e=>{a.error(e),l&&a.summary.addRaw(`\u274C ${e}
`)},success:e=>{let t=`\u2705 ${e}`;a.info(t),l&&a.summary.addRaw(`${t}
`)},debug:e=>{X&&(l?a.debug(e):a.info(`[DEBUG] ${e}`))},box:oe,summaryTable:ne,separator:re,writeSummary:async()=>{l&&await a.summary.write()},startGroup:Z,endGroup:ee};import{createSign as ie}from"node:crypto";import*as w from"@actions/core";function se(){let e=w.getInput("github_installation_token"),t=process.env.GITHUB_INSTALLATION_TOKEN;return e||t||null}function ae(){return!!process.env.GITHUB_ACTIONS}async function ce(){i.info("Generating OIDC token...");let e=await w.getIDToken("pullfrog-api");i.info("OIDC token generated successfully");let t=process.env.API_URL||"https://pullfrog.ai";i.info("Exchanging OIDC token for installation token...");let o=await fetch(`${t}/api/github/installation-token`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"}});if(!o.ok){let s=await o.text();throw new Error(`Token exchange failed: ${o.status} ${o.statusText} - ${s}`)}let n=await o.json();return i.info(`Installation token obtained for ${n.repository||"all repositories"}`),n.token}var E=e=>Buffer.from(e).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,""),pe=(e,t)=>{let o=Math.floor(Date.now()/1e3),n={iat:o-60,exp:o+300,iss:e},r=E(JSON.stringify({alg:"RS256",typ:"JWT"})),p=E(JSON.stringify(n)),c=`${r}.${p}`,u=ie("RSA-SHA256").update(c).sign(t,"base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"");return`${c}.${u}`},S=async(e,t={})=>{let{method:o="GET",headers:n={},body:s}=t,r=`https://api.github.com${e}`,p={Accept:"application/vnd.github.v3+json","User-Agent":"Pullfrog-Installation-Token-Generator/1.0",...n},c=await fetch(r,{method:o,headers:p,...s&&{body:s}});if(!c.ok){let u=await c.text();throw new Error(`GitHub API request failed: ${c.status} ${c.statusText}
${u}`)}return c.json()},ue=async(e,t,o)=>{try{return(await S("/installation/repositories",{headers:{Authorization:`token ${e}`}})).repositories.some(s=>s.owner.login===t&&s.name===o)}catch{return!1}},M=async(e,t)=>(await S(`/app/installations/${t}/access_tokens`,{method:"POST",headers:{Authorization:`Bearer ${e}`}})).token,le=async(e,t,o)=>{let n=await S("/app/installations",{headers:{Authorization:`Bearer ${e}`}});for(let s of n)try{let r=await M(e,s.id);if(await ue(r,t,o))return s.id}catch{}throw new Error(`No installation found with access to ${t}/${o}. Ensure the GitHub App is installed on the target repository.`)};async function me(){let e=T(),t={appId:process.env.GITHUB_APP_ID,privateKey:process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g,`
`),repoOwner:e.owner,repoName:e.name},o=pe(t.appId,t.privateKey),n=await le(o,t.repoOwner,t.repoName);return await M(o,n)}async function de(){return ae()?await ce():await me()}async function O(){let e=se();if(e)return w.setSecret(e),i.info("Using provided GitHub installation token"),e;let t=await de();return w.setSecret(t),process.env.GITHUB_INSTALLATION_TOKEN=t,t}function T(){let e=process.env.GITHUB_REPOSITORY;if(!e)throw new Error("GITHUB_REPOSITORY environment variable is required");let[t,o]=e.split("/");if(!t||!o)throw new Error(`Invalid GITHUB_REPOSITORY format: ${e}. Expected 'owner/repo'`);return{owner:t,name:o}}var x="gh-pullfrog";function G(e){let t=T(),o=`${t.owner}/${t.name}`,n=process.env.GITHUB_ACTION_PATH?`${process.env.GITHUB_ACTION_PATH}/entry.js`:`${process.cwd()}/entry.js`;return{[x]:{command:"node",args:["--input-type=module","-e",`import('${n.replace(/'/g,"\\'")}').then(m => m.createMcpServer())`],env:{GITHUB_INSTALLATION_TOKEN:e,GITHUB_REPOSITORY:o}}}}var H=`
You are a highly intelligent, no-nonsense senior-level software engineering agent. You are careful, to-the-point, and kind. You only say things you know to be true. Your code is focused, minimal, and production-ready. You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so. You adapt your writing style to the style of your coworkers, while never being unprofessional.
- eagerly inspect your MCP servers to determine what tools are available to you, especially ${x}
- do not under any circumstances use the github cli (\`gh\`). find the corresponding tool from ${x} instead.
- mode selection: choose the appropriate mode based on the prompt payload:
- choose "plan mode" if the prompt asks to:
- create a plan, break down tasks, outline steps, or analyze requirements
- understand the scope of work before implementation
- provide a todo list or task breakdown
- choose "implement" if the prompt asks to:
- implement, build, create, or develop code changes
- make specific changes to files or features
- execute a plan that was previously created
- the prompt includes specific implementation details or requirements
- choose "review" if the prompt asks to:
- review code, PR, or implementation
- provide feedback, suggestions, or identify issues
- check code quality, style, or correctness
- once you've chosen a mode, follow its associated prompts carefully
- when prompted directly (e.g., via issue comment or PR comment):
(1) start by creating a single response comment using mcp__${x}__create_issue_comment
- the initial comment should say something like "I'll do {summary of request}" where you summarize what was requested
- save the commentId returned from this initial comment creation
(2) use mcp__${x}__edit_issue_comment to progressively update that same comment as you make progress
- update the comment with current status, completed tasks, and any relevant information
- continue updating the same comment throughout the planning/implementation process
(3) create_issue_comment should only be used once initially - all subsequent updates must use edit_issue_comment with the saved commentId
- if prompted to review a PR:
(1) get PR info with mcp__${x}__get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
(2) view diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments)
(3) read files from the checked-out PR branch to understand the implementation
(4) when submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
(5) only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
replace <base> and <head> with 'base' and 'head' from the PR info
`;var N={run:async({prompt:e,mcpServers:t,apiKey:o})=>{process.env.ANTHROPIC_API_KEY=o;let n=fe({prompt:`${H}
${e}`,options:{permissionMode:"bypassPermissions",mcpServers:t}});for await(let s of n){let r=ge[s.type];await r(s)}return{success:!0,output:""}}},C=new Set,ge={assistant:e=>{if(e.message?.content){for(let t of e.message.content)if(t.type==="text"&&t.text?.trim())i.box(t.text.trim(),{title:"Claude"});else if(t.type==="tool_use"&&(i.info(`\u2192 ${t.name}`),t.name==="bash"&&t.id&&C.add(t.id),t.input)){let o=t.input;if(o.description&&i.info(` \u2514\u2500 ${o.description}`),o.command&&i.info(` \u2514\u2500 command: ${o.command}`),o.file_path&&i.info(` \u2514\u2500 file: ${o.file_path}`),o.content){let n=o.content.length>100?`${o.content.substring(0,100)}...`:o.content;i.info(` \u2514\u2500 content: ${n}`)}o.query&&i.info(` \u2514\u2500 query: ${o.query}`),o.pattern&&i.info(` \u2514\u2500 pattern: ${o.pattern}`),o.url&&i.info(` \u2514\u2500 url: ${o.url}`),o.edits&&Array.isArray(o.edits)&&(i.info(` \u2514\u2500 edits: ${o.edits.length} changes`),o.edits.forEach((n,s)=>{n.file_path&&i.info(` ${s+1}. ${n.file_path}`)})),o.task&&i.info(` \u2514\u2500 task: ${o.task}`),o.bash_command&&i.info(` \u2514\u2500 bash_command: ${o.bash_command}`)}}},user:e=>{if(e.message?.content){for(let t of e.message.content)if(t.type==="tool_result"){let o=t.tool_use_id;if(o&&C.has(o)){let s=typeof t.content=="string"?t.content:Array.isArray(t.content)?t.content.map(r=>typeof r=="string"?r:r.text||JSON.stringify(r)).join(`
`):String(t.content);i.startGroup("bash output"),t.is_error?i.warning(s):i.info(s),i.endGroup(),C.delete(o)}else if(t.is_error){let s=typeof t.content=="string"?t.content:String(t.content);i.warning(`Tool error: ${s}`)}}}},result:async e=>{e.subtype==="success"?await i.summaryTable([[{data:"Cost",header:!0},{data:"Input Tokens",header:!0},{data:"Output Tokens",header:!0}],[`$${e.total_cost_usd?.toFixed(4)||"0.0000"}`,String(e.usage?.input_tokens||0),String(e.usage?.output_tokens||0)]]):e.subtype==="error_max_turns"?i.error(`Max turns reached: ${JSON.stringify(e)}`):e.subtype==="error_during_execution"?i.error(`Execution error: ${JSON.stringify(e)}`):i.error(`Failed: ${JSON.stringify(e)}`)},system:()=>{},stream_event:()=>{}};import{execSync as I}from"node:child_process";function q(){i.info("\u{1F527} Setting up git configuration..."),I('git config user.email "action@pullfrog.ai"',{stdio:"inherit"}),I('git config user.name "Pullfrog Action"',{stdio:"inherit"})}function U(e,t){if(!process.env.GITHUB_ACTIONS){i.info("\u26A0\uFE0F Skipping git authentication setup (not in GitHub Actions)");return}i.info("\u{1F510} Setting up git authentication...");try{I("git config --unset-all http.https://github.com/.extraheader",{stdio:"inherit"}),i.info("\u2713 Removed existing authentication headers")}catch{i.info("No existing authentication headers to remove")}let o=`https://x-access-token:${e}@github.com/${t.owner}/${t.name}.git`;I(`git remote set-url origin "${o}"`,{stdio:"inherit"}),i.info("\u2713 Updated remote URL with authentication token")}var D=he({prompt:"string","anthropic_api_key?":"string | undefined"});async function B(e){try{i.info("Starting agent run..."),q();let t=await O(),o=T();U(t,o);let n=G(t);i.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(n,null,2)}`),i.info("Running Claude Agent SDK..."),i.box(e.prompt,{title:"Prompt"});let s=await N.run({prompt:e.prompt,mcpServers:n,githubInstallationToken:t,apiKey:e.anthropic_api_key});return s.success?(i.success("Task complete."),await i.writeSummary(),{success:!0,output:s.output||""}):{success:!1,error:s.error||"Agent execution failed",output:s.output}}catch(t){let o=t instanceof Error?t.message:"Unknown error occurred";return i.error(o),await i.writeSummary(),{success:!1,error:o}}}import{FastMCP as Ce}from"fastmcp";import{type as _}from"arktype";import{cached as ye}from"@ark/util";import{Octokit as be}from"@octokit/rest";var we=ye(()=>{let e=process.env.GITHUB_INSTALLATION_TOKEN;if(!e)throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");return{...T(),octokit:new be({auth:e})}}),m=e=>e,L=(e,t)=>{for(let o of t)e.addTool(o);return e},d=e=>async t=>{try{let o=we(),n=await e(t,o);return Te(n)}catch(o){return xe(o)}},Te=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]}),xe=e=>({content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0});var _e=_({issueNumber:_.number.describe("the issue number to comment on"),body:_.string.describe("the comment body content")}),K=m({name:"create_issue_comment",description:"Create a comment on a GitHub issue",parameters:_e,execute:d(async({issueNumber:e,body:t},o)=>{let n=await o.octokit.rest.issues.createComment({owner:o.owner,repo:o.name,issue_number:e,body:t});return{success:!0,commentId:n.data.id,url:n.data.html_url,body:n.data.body}})}),ke=_({commentId:_.number.describe("the ID of the comment to edit"),body:_.string.describe("the new comment body content")}),j=m({name:"edit_issue_comment",description:"Edit a GitHub issue comment by its ID",parameters:ke,execute:d(async({commentId:e,body:t},o)=>{let n=await o.octokit.rest.issues.updateComment({owner:o.owner,repo:o.name,comment_id:e,body:t});return{success:!0,commentId:n.data.id,url:n.data.html_url,body:n.data.body,updatedAt:n.data.updated_at}})});import{type as R}from"arktype";var ve=R({title:R.string.describe("the title of the issue"),body:R.string.describe("the body content of the issue"),labels:R.string.array().describe("optional array of label names to apply to the issue").optional(),assignees:R.string.array().describe("optional array of usernames to assign to the issue").optional()}),J=m({name:"create_issue",description:"Create a new GitHub issue",parameters:ve,execute:d(async({title:e,body:t,labels:o,assignees:n},s)=>{let r=await s.octokit.rest.issues.create({owner:s.owner,repo:s.name,title:e,body:t,labels:o??[],assignees:n??[]});return{success:!0,issueId:r.data.id,number:r.data.number,url:r.data.html_url,title:r.data.title,state:r.data.state,labels:r.data.labels?.map(p=>typeof p=="string"?p:p.name),assignees:r.data.assignees?.map(p=>p.login)}})});import{execSync as Re}from"node:child_process";import{type as $}from"arktype";var Ie=$({title:$.string.describe("the title of the pull request"),body:$.string.describe("the body content of the pull request"),base:$.string.describe("the base branch to merge into (e.g., 'main')")}),z=m({name:"create_pull_request",description:"Create a pull request from the current branch",parameters:Ie,execute:d(async({title:e,body:t,base:o},n)=>{let s=Re("git rev-parse --abbrev-ref HEAD",{encoding:"utf8"}).trim();i.info(`Current branch: ${s}`);let r=await n.octokit.rest.pulls.create({owner:n.owner,repo:n.name,title:e,body:t,head:s,base:o});return{success:!0,pullRequestId:r.data.id,number:r.data.number,url:r.data.html_url,title:r.data.title,head:r.data.head.ref,base:r.data.base.ref}})});import{execSync as A}from"node:child_process";import{type as Y}from"arktype";var $e=Y({pull_number:Y.number.describe("The pull request number to fetch")}),F=m({name:"get_pull_request",description:"Retrieve PR information and automatically prepare the repository for review by fetching and checking out the PR branch.",parameters:$e,execute:d(async({pull_number:e},t)=>{let n=(await t.octokit.rest.pulls.get({owner:t.owner,repo:t.name,pull_number:e})).data,s=n.base.ref,r=n.head.ref;if(!s)throw new Error(`Base branch not found for PR #${e}`);return i.info(`Fetching base branch: origin/${s}`),A(`git fetch origin ${s} --depth=20`,{stdio:"inherit"}),i.info(`Fetching PR branch: origin/${r}`),A(`git fetch origin ${r}`,{stdio:"inherit"}),i.info(`Checking out PR branch: origin/${r}`),A(`git checkout origin/${r}`,{stdio:"inherit"}),{number:n.number,url:n.html_url,title:n.title,state:n.state,draft:n.draft,merged:n.merged,base:s,head:r}})});import{type as f}from"arktype";var Se=f({pull_number:f.number.describe("The pull request number to review"),event:f.enumerated("APPROVE","REQUEST_CHANGES","COMMENT").describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"),body:f.string.describe("Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array.").optional(),commit_id:f.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(),comments:f({path:f.string.describe("The file path to comment on (relative to repo root)"),line:f.number.describe("The line number in the file (use line numbers from the diff - usually the RIGHT side/new code)"),side:f.enumerated("LEFT","RIGHT").describe("Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided.").optional(),body:f.string.describe("The comment text for this specific line"),start_line:f.number.describe("Start line for multi-line comments (optional, for commenting on ranges)").optional()}).array().describe("REQUIRED: Array of inline comments for specific code issues. Use this for all location-specific feedback. Use 'git diff origin/<base>...origin/<head>' to find the correct line numbers (typically use the line numbers shown on the RIGHT side for new code, LEFT side for old code).").optional()}),V=m({name:"submit_pull_request_review",description:"Submit a review (approve, request changes, or comment) for an existing pull request. IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.",parameters:Se,execute:d(async({pull_number:e,event:t,body:o,commit_id:n,comments:s=[]},r)=>{let p=await r.octokit.rest.pulls.get({owner:r.owner,repo:r.name,pull_number:e}),c={owner:r.owner,repo:r.name,pull_number:e,event:t};o&&(c.body=o),n?c.commit_id=n:c.commit_id=p.data.head.sha,s.length>0&&(c.comments=s.map(y=>{let g={...y};return g.side=y.side||"RIGHT",y.start_line&&(g.start_line=y.start_line,g.start_side=y.side||"RIGHT"),g}));let u=await r.octokit.rest.pulls.createReview(c);return{success:!0,reviewId:u.data.id,html_url:u.data.html_url,state:u.data.state,user:u.data.user?.login,submitted_at:u.data.submitted_at}})});function Ae(){let e=new Ce({name:"gh-pullfrog",version:"0.0.1"});L(e,[K,j,J,z,V,F]),e.start()}var W={name:"@pullfrog/action",version:"0.0.76",type:"module",files:["index.js","index.cjs","index.d.ts","index.d.cts","agents","utils","main.js","main.d.ts"],scripts:{test:'echo "Error: no test specified" && exit 1',typecheck:"tsc --noEmit",build:"node esbuild.config.js",play:"node play.ts",upDeps:"pnpm up --latest",lock:"pnpm --ignore-workspace install",prepare:"husky"},dependencies:{"@actions/core":"^1.11.1","@anthropic-ai/claude-agent-sdk":"^0.1.26","@ark/fs":"0.53.0","@ark/util":"0.53.0","@octokit/rest":"^22.0.0","@octokit/webhooks-types":"^7.6.1","@standard-schema/spec":"1.0.0",arktype:"2.1.25",dotenv:"^17.2.3",execa:"^9.6.0",fastmcp:"^3.20.0",table:"^6.9.0"},devDependencies:{"@types/node":"^24.7.2",arg:"^5.0.2",esbuild:"^0.25.9",husky:"^9.0.0",typescript:"^5.9.3"},repository:{type:"git",url:"git+https://github.com/pullfrog/action.git"},keywords:[],author:"",license:"MIT",bugs:{url:"https://github.com/pullfrog/action/issues"},homepage:"https://github.com/pullfrog/action#readme",zshy:{exports:"./index.ts"},main:"./dist/index.cjs",module:"./dist/index.js",types:"./dist/index.d.cts",exports:{".":{types:"./dist/index.d.cts",import:"./dist/index.js",require:"./dist/index.cjs"}}};async function Me(){try{i.info(`\u{1F438} Running pullfrog/action@${W.version}...`);let e=process.env.INPUTS_JSON;if(!e)throw new Error("INPUTS_JSON environment variable not found");let t=Ee("string.json.parse").assert(e),o=D.assert(t),n=await B(o);if(!n.success)throw new Error(n.error||"Agent execution failed")}catch(e){let t=e instanceof Error?e.message:"Unknown error occurred";Q.setFailed(`Action failed: ${t}`)}}await Me();export{Ae as createMcpServer};