merge main

This commit is contained in:
Shawn Morreau
2025-11-14 16:14:36 -05:00
11 changed files with 8274 additions and 7791 deletions
+9
View File
@@ -13,6 +13,15 @@ inputs:
openai_api_key: openai_api_key:
description: "OpenAI API key for Codex authentication" description: "OpenAI API key for Codex authentication"
required: false required: false
google_api_key:
description: "Google API key for Jules authentication"
required: false
gemini_api_key:
description: "Gemini API key for Jules authentication"
required: false
cursor_api_key:
description: "Cursor API key for Cursor authentication"
required: false
runs: runs:
using: "node20" using: "node20"
+1 -1
View File
@@ -6,7 +6,7 @@ import { agent, installFromNpmTarball } from "./shared.ts";
export const claude = agent({ export const claude = agent({
name: "claude", name: "claude",
inputKey: "anthropic_api_key", inputKeys: ["anthropic_api_key"],
install: async () => { install: async () => {
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest"; const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
return await installFromNpmTarball({ return await installFromNpmTarball({
+1 -1
View File
@@ -7,7 +7,7 @@ import { agent, installFromNpmTarball } from "./shared.ts";
export const codex = agent({ export const codex = agent({
name: "codex", name: "codex",
inputKey: "openai_api_key", inputKeys: ["openai_api_key"],
install: async () => { install: async () => {
return await installFromNpmTarball({ return await installFromNpmTarball({
packageName: "@openai/codex", packageName: "@openai/codex",
+1 -1
View File
@@ -10,4 +10,4 @@ export const agents = {
jules, jules,
} as const; } as const;
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKey"]; export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number];
+2 -18
View File
@@ -6,7 +6,7 @@ import { agent, installFromNpmTarball } from "./shared.ts";
export const jules = agent({ export const jules = agent({
name: "jules", name: "jules",
inputKey: "google_api_key", inputKeys: ["google_api_key", "gemini_api_key"],
install: async () => { install: async () => {
return await installFromNpmTarball({ return await installFromNpmTarball({
packageName: "@google/jules", packageName: "@google/jules",
@@ -24,26 +24,13 @@ export const jules = agent({
if (!apiKey) { if (!apiKey) {
throw new Error("google_api_key is required for jules agent"); throw new Error("google_api_key is required for jules agent");
} }
process.env.GOOGLE_API_KEY = apiKey;
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
const repoName = `${repoContext.owner}/${repoContext.name}`; const repoName = `${repoContext.owner}/${repoContext.name}`;
log.info(`Creating Jules session for ${repoName}...`); log.info(`Creating Jules session for ${repoName}...`);
// Set API key as environment variable for CLI authentication
// Note: The CLI may require browser-based auth via 'jules login' in interactive mode
// In CI, we rely on the API key being set as an environment variable
const env: Record<string, string> = {
GOOGLE_API_KEY: apiKey,
JULES_API_KEY: apiKey,
};
// Copy over existing env vars, filtering out undefined values
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
env[key] = value;
}
}
// Create a new remote session // Create a new remote session
const sessionPrompt = addInstructions(prompt); const sessionPrompt = addInstructions(prompt);
log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`); log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`);
@@ -53,7 +40,6 @@ export const jules = agent({
const createResult = await spawn({ const createResult = await spawn({
cmd: "node", cmd: "node",
args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt], args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt],
env,
onStdout: (chunk) => { onStdout: (chunk) => {
log.info(chunk.trim()); log.info(chunk.trim());
// Try to extract session ID from output // Try to extract session ID from output
@@ -114,7 +100,6 @@ export const jules = agent({
const listResult = await spawn({ const listResult = await spawn({
cmd: "node", cmd: "node",
args: [cliPath, "remote", "list", "--session"], args: [cliPath, "remote", "list", "--session"],
env,
onStdout: (chunk) => { onStdout: (chunk) => {
// Log session updates // Log session updates
const trimmed = chunk.trim(); const trimmed = chunk.trim();
@@ -155,7 +140,6 @@ export const jules = agent({
const pullResult = await spawn({ const pullResult = await spawn({
cmd: "node", cmd: "node",
args: [cliPath, "remote", "pull", "--session", sessionId], args: [cliPath, "remote", "pull", "--session", sessionId],
env,
onStdout: (chunk) => { onStdout: (chunk) => {
log.info(chunk.trim()); log.info(chunk.trim());
}, },
+1 -1
View File
@@ -207,7 +207,7 @@ export const agent = <const agent extends Agent>(agent: agent): agent => {
export type Agent = { export type Agent = {
name: string; name: string;
inputKey: string; inputKeys: string[];
install: () => Promise<string>; install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>; run: (config: AgentConfig) => Promise<AgentResult>;
}; };
+8190 -7755
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -23,7 +23,9 @@ async function run(): Promise<void> {
const inputs: Required<Inputs> = { const inputs: Required<Inputs> = {
prompt: core.getInput("prompt", { required: true }), prompt: core.getInput("prompt", { required: true }),
agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined, 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); const result = await main(inputs);
+62 -8
View File
@@ -7,6 +7,7 @@ import { fetchRepoSettings } from "./utils/api.ts";
import { log } from "./utils/cli.ts"; import { log } from "./utils/cli.ts";
import { import {
parseRepoContext, parseRepoContext,
type RepoContext,
revokeInstallationToken, revokeInstallationToken,
setupGitHubInstallationToken, setupGitHubInstallationToken,
} from "./utils/github.ts"; } 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 type AgentName = typeof AgentName.infer;
export const AgentInputKey = type.enumerated( 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; export type AgentInputKey = typeof AgentInputKey.infer;
const keyInputDefs = flatMorph( const keyInputDefs = flatMorph(agents, (_, agent) =>
agents, agent.inputKeys.map((inputKey) => [inputKey, "string | undefined?"] as const)
(_, agent) => [agent.inputKey, "string | undefined?"] as const
); );
export const Inputs = type({ export const Inputs = type({
@@ -39,7 +39,54 @@ export interface MainResult {
error?: string | undefined; 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<MainResult> { export async function main(inputs: Inputs): Promise<MainResult> {
let tokenToRevoke: string | null = null; let tokenToRevoke: string | null = null;
@@ -81,11 +128,18 @@ export async function main(inputs: Inputs): Promise<MainResult> {
// if yes, check if it's a webhook payload or toJSON(github.event) // if yes, check if it's a webhook payload or toJSON(github.event)
// for webhook payloads, check the specified `agent` field // 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({ const result = await agent.run({
prompt: inputs.prompt, prompt: inputs.prompt,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/action", "name": "@pullfrog/action",
"version": "0.0.104", "version": "0.0.105",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
+3 -4
View File
@@ -25,10 +25,9 @@ export async function run(
const inputs: Required<Inputs> = { const inputs: Required<Inputs> = {
prompt, prompt,
agent: "cursor", agent: "cursor",
...flatMorph(agents, (_, agent) => [ ...flatMorph(agents, (_, agent) =>
agent.inputKey, agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
process.env[agent.inputKey.toUpperCase()], ),
]),
}; };
const result = await main(inputs); const result = await main(inputs);