diff --git a/agents/claude.ts b/agents/claude.ts index 6a35521..a00a036 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -6,7 +6,7 @@ import { agent, installFromNpmTarball } from "./shared.ts"; export const claude = agent({ name: "claude", - inputKey: "anthropic_api_key", + inputKeys: ["anthropic_api_key"], install: async () => { const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest"; return await installFromNpmTarball({ diff --git a/agents/codex.ts b/agents/codex.ts index 6601f41..3506eec 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -7,7 +7,7 @@ import { agent, installFromNpmTarball } from "./shared.ts"; export const codex = agent({ name: "codex", - inputKey: "openai_api_key", + inputKeys: ["openai_api_key"], install: async () => { return await installFromNpmTarball({ packageName: "@openai/codex", diff --git a/agents/index.ts b/agents/index.ts index af18fac..b2f2a75 100644 --- a/agents/index.ts +++ b/agents/index.ts @@ -8,4 +8,4 @@ export const agents = { jules, } as const; -export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKey"]; +export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number]; diff --git a/agents/jules.ts b/agents/jules.ts index 8dd9af7..9cf68dd 100644 --- a/agents/jules.ts +++ b/agents/jules.ts @@ -6,7 +6,7 @@ import { agent, installFromNpmTarball } from "./shared.ts"; export const jules = agent({ name: "jules", - inputKey: "google_api_key", + inputKeys: ["google_api_key", "gemini_api_key"], install: async () => { return await installFromNpmTarball({ packageName: "@google/jules", diff --git a/agents/shared.ts b/agents/shared.ts index 82739b4..22cb0e5 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -133,7 +133,7 @@ export const agent = (agent: agent): agent => { export type Agent = { name: string; - inputKey: string; + inputKeys: string[]; install: () => Promise; run: (config: AgentConfig) => Promise; }; diff --git a/entry.ts b/entry.ts index 1dde2de..01e6595 100644 --- a/entry.ts +++ b/entry.ts @@ -23,7 +23,9 @@ async function run(): Promise { const inputs: Required = { prompt: core.getInput("prompt", { required: true }), agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined, - ...flatMorph(agents, (_, agent) => [agent.inputKey, core.getInput(agent.inputKey)]), + ...flatMorph(agents, (_, agent) => + agent.inputKeys.map((inputKey) => [inputKey, core.getInput(inputKey)]) + ), }; const result = await main(inputs); diff --git a/main.ts b/main.ts index b06126d..dd09ff4 100644 --- a/main.ts +++ b/main.ts @@ -7,6 +7,7 @@ import { fetchRepoSettings } from "./utils/api.ts"; import { log } from "./utils/cli.ts"; import { parseRepoContext, + type RepoContext, revokeInstallationToken, setupGitHubInstallationToken, } from "./utils/github.ts"; @@ -16,13 +17,12 @@ export const AgentName = type.enumerated(...Object.values(agents).map((agent) => export type AgentName = typeof AgentName.infer; export const AgentInputKey = type.enumerated( - ...Object.values(agents).map((agent) => agent.inputKey) + ...Object.values(agents).flatMap((agent) => agent.inputKeys) ); export type AgentInputKey = typeof AgentInputKey.infer; -const keyInputDefs = flatMorph( - agents, - (_, agent) => [agent.inputKey, "string | undefined?"] as const +const keyInputDefs = flatMorph(agents, (_, agent) => + agent.inputKeys.map((inputKey) => [inputKey, "string | undefined?"] as const) ); export const Inputs = type({ @@ -39,7 +39,54 @@ export interface MainResult { error?: string | undefined; } -export type PromptJSON = {}; +/** + * Throw an error for missing API key with helpful message linking to repo settings + */ +function throwMissingApiKeyError({ + agentName, + inputKeys, + repoContext, + inputs, +}: { + agentName: string; + inputKeys: string[]; + repoContext: RepoContext; + inputs: Inputs; +}): never { + const apiUrl = process.env.API_URL || "https://pullfrog.ai"; + const settingsUrl = `${apiUrl}/console/${repoContext.owner}/${repoContext.name}`; + + const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``); + const secretNameList = + inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`; + + const githubRepoUrl = `https://github.com/${repoContext.owner}/${repoContext.name}`; + const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`; + + // Find which agents have inputKeys that match the provided inputs + const availableAgents = Object.values(agents).filter((agent) => + agent.inputKeys.some((inputKey) => inputs[inputKey]) + ); + + let message = `Pullfrog is configured to use ${agentName}, but the associated API key was not provided. + +To fix this, add the required secret to your GitHub repository: + +1. Go to: ${githubSecretsUrl} +2. Click "New repository secret" +3. Set the name to ${secretNameList} +4. Set the value to your API key +5. Click "Add secret"`; + + // If other credentials are present, suggest alternative agents + if (availableAgents.length > 0) { + const agentNames = availableAgents.map((agent) => agent.name).join(", "); + message += `\n\nAlternatively, configure Pullfrog to use an agent with existing credentials in your environment (${agentNames}) at ${settingsUrl}`; + } + + log.error(message); + throw new Error(message); +} export async function main(inputs: Inputs): Promise { let tokenToRevoke: string | null = null; @@ -81,11 +128,18 @@ export async function main(inputs: Inputs): Promise { // if yes, check if it's a webhook payload or toJSON(github.event) // for webhook payloads, check the specified `agent` field - // Get API key based on agent type + const matchingInputKey = agent.inputKeys.find((inputKey) => inputs[inputKey]); - const apiKey = inputs[agent.inputKey]; + if (!matchingInputKey) { + throwMissingApiKeyError({ + agentName, + inputKeys: agent.inputKeys, + repoContext, + inputs, + }); + } - if (!apiKey) throw new Error(`${agent.inputKey} is required for ${agentName} agent`); + const apiKey = inputs[matchingInputKey]!; const result = await agent.run({ prompt: inputs.prompt, diff --git a/play.ts b/play.ts index 94d9eec..276111c 100644 --- a/play.ts +++ b/play.ts @@ -24,11 +24,10 @@ export async function run( const inputs: Required = { prompt, - agent: "jules", - ...flatMorph(agents, (_, agent) => [ - agent.inputKey, - process.env[agent.inputKey.toUpperCase()], - ]), + agent: "claude", + ...flatMorph(agents, (_, agent) => + agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) + ), }; const result = await main(inputs);