Add footer links

This commit is contained in:
Colin McDonnell
2025-11-20 16:05:07 -08:00
parent f8bb2e12f3
commit 6c6b7b0b2d
4 changed files with 81 additions and 12 deletions
+1 -1
View File
@@ -226,7 +226,7 @@ function parsePayload(inputs: Inputs): Payload {
function setupMcpServers(ctx: MainContext): void { function setupMcpServers(ctx: MainContext): void {
const allModes = [...modes, ...(ctx.payload.modes || [])]; const allModes = [...modes, ...(ctx.payload.modes || [])];
ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes); ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes, ctx.payload);
log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`); log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
} }
+41 -2
View File
@@ -1,6 +1,39 @@
import { type } from "arktype"; import { type } from "arktype";
import type { Payload } from "../external.ts";
import { agentsManifest } from "../external.ts";
import { parseRepoContext } from "../utils/github.ts";
import { contextualize, tool } from "./shared.ts"; import { contextualize, tool } from "./shared.ts";
function buildCommentFooter(payload: Payload): string {
const repoContext = parseRepoContext();
const runId = process.env.GITHUB_RUN_ID;
const agentName = payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
const agentDisplayName = agentInfo?.displayName || "Unknown Agent";
// agent URLs based on manifest
const agentUrls: Record<string, string> = {
claude: "https://claude.com/claude-code",
codex: "https://platform.openai.com/docs/guides/codex",
cursor: "https://cursor.com/",
gemini: "https://ai.google.dev/gemini-api/docs",
};
const agentUrl = agentName
? agentUrls[agentName] || "https://pullfrog.ai"
: "https://pullfrog.ai";
// build workflow run URL
const workflowRunUrl = runId
? `https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId}`
: `https://github.com/${repoContext.owner}/${repoContext.name}`;
return `---
<img src="https://pullfrog.ai/pullfrog-logo.png" width="12px" height="12px" style="vertical-align: middle;" /> <sup>Triggered by [Pullfrog](https://pullfrog.ai) | Using [${agentDisplayName}](${agentUrl}) | [View workflow run](${workflowRunUrl}) | [![Share on X](https://img.shields.io/badge/X-share-black)](https://x.com/pullfrogai)</sup>`;
}
export const Comment = type({ export const Comment = type({
issueNumber: type.number.describe("the issue number to comment on"), issueNumber: type.number.describe("the issue number to comment on"),
body: type.string.describe("the comment body content"), body: type.string.describe("the comment body content"),
@@ -73,11 +106,14 @@ export const CreateWorkingCommentTool = tool({
throw new Error("create_working_comment may not be called multiple times"); throw new Error("create_working_comment may not be called multiple times");
} }
const footer = buildCommentFooter(ctx.payload);
const body = `${intent} <img src="https://pullfrog.ai/party-parrot.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />${footer}`;
const result = await ctx.octokit.rest.issues.createComment({ const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner, owner: ctx.owner,
repo: ctx.name, repo: ctx.name,
issue_number: issueNumber, issue_number: issueNumber,
body: `${intent} <img src="https://pullfrog.ai/party-parrot.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`, body,
}); });
workingCommentId = result.data.id; workingCommentId = result.data.id;
@@ -104,11 +140,14 @@ export const UpdateWorkingCommentTool = tool({
throw new Error("create_working_comment must be called before update_working_comment"); throw new Error("create_working_comment must be called before update_working_comment");
} }
const footer = buildCommentFooter(ctx.payload);
const bodyWithFooter = `${body}${footer}`;
const result = await ctx.octokit.rest.issues.updateComment({ const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner, owner: ctx.owner,
repo: ctx.name, repo: ctx.name,
comment_id: workingCommentId, comment_id: workingCommentId,
body, body: bodyWithFooter,
}); });
return { return {
+19 -6
View File
@@ -4,6 +4,7 @@
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs"; import { fromHere } from "@ark/fs";
import type { Payload } from "../external.ts";
import { ghPullfrogMcpName } from "../external.ts"; import { ghPullfrogMcpName } from "../external.ts";
import type { Mode } from "../modes.ts"; import type { Mode } from "../modes.ts";
import { parseRepoContext } from "../utils/github.ts"; import { parseRepoContext } from "../utils/github.ts";
@@ -12,7 +13,11 @@ export type McpName = typeof ghPullfrogMcpName;
export type McpConfigs = Record<McpName, McpStdioServerConfig>; export type McpConfigs = Record<McpName, McpStdioServerConfig>;
export function createMcpConfigs(githubInstallationToken: string, modes: Mode[]): McpConfigs { export function createMcpConfigs(
githubInstallationToken: string,
modes: Mode[],
payload: Payload
): McpConfigs {
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`; const githubRepository = `${repoContext.owner}/${repoContext.name}`;
@@ -20,15 +25,23 @@ export function createMcpConfigs(githubInstallationToken: string, modes: Mode[])
// In development, server.ts is in the same directory as this file (config.ts) // In development, server.ts is in the same directory as this file (config.ts)
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server") : fromHere("server.ts"); const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server") : fromHere("server.ts");
const env: Record<string, string> = {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
PULLFROG_MODES: JSON.stringify(modes),
PULLFROG_PAYLOAD: JSON.stringify(payload),
};
// pass through GITHUB_RUN_ID if available (automatically set in GitHub Actions)
if (process.env.GITHUB_RUN_ID) {
env.GITHUB_RUN_ID = process.env.GITHUB_RUN_ID;
}
return { return {
[ghPullfrogMcpName]: { [ghPullfrogMcpName]: {
command: "node", command: "node",
args: [serverPath], args: [serverPath],
env: { env,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
PULLFROG_MODES: JSON.stringify(modes),
},
}, },
}; };
} }
+20 -3
View File
@@ -1,7 +1,7 @@
import { cached } from "@ark/util";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import type { StandardSchemaV1 } from "@standard-schema/spec"; import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { FastMCP, Tool } from "fastmcp"; import type { FastMCP, Tool } from "fastmcp";
import type { Payload } from "../external.ts";
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
import { parseRepoContext, type RepoContext } from "../utils/github.ts"; import { parseRepoContext, type RepoContext } from "../utils/github.ts";
@@ -13,7 +13,22 @@ export interface ToolResult {
isError?: boolean; isError?: boolean;
} }
export const getMcpContext = cached((): McpContext => { export function getPayload(): Payload {
const payloadEnv = process.env.PULLFROG_PAYLOAD;
if (!payloadEnv) {
throw new Error("PULLFROG_PAYLOAD environment variable is required");
}
try {
return JSON.parse(payloadEnv) as Payload;
} catch (error) {
throw new Error(
`Failed to parse PULLFROG_PAYLOAD: ${error instanceof Error ? error.message : String(error)}`
);
}
}
export function getMcpContext(): McpContext {
const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN; const githubInstallationToken = process.env.GITHUB_INSTALLATION_TOKEN;
if (!githubInstallationToken) { if (!githubInstallationToken) {
throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required"); throw new Error("GITHUB_INSTALLATION_TOKEN environment variable is required");
@@ -24,11 +39,13 @@ export const getMcpContext = cached((): McpContext => {
octokit: new Octokit({ octokit: new Octokit({
auth: githubInstallationToken, auth: githubInstallationToken,
}), }),
payload: getPayload(),
}; };
}); }
export interface McpContext extends RepoContext { export interface McpContext extends RepoContext {
octokit: Octokit; octokit: Octokit;
payload: Payload;
} }
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => { export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {