Switch to payload

This commit is contained in:
Colin McDonnell
2025-11-18 23:15:44 -08:00
parent 3ef1635bb6
commit 06a19567c0
13 changed files with 614 additions and 410 deletions
+2 -2
View File
@@ -15,11 +15,11 @@ export const claude = agent({
executablePath: "cli.js", executablePath: "cli.js",
}); });
}, },
run: async ({ prompt, mcpServers, apiKey, cliPath }) => { run: async ({ payload, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey; process.env.ANTHROPIC_API_KEY = apiKey;
const queryInstance = query({ const queryInstance = query({
prompt: addInstructions(prompt), prompt: addInstructions(payload),
options: { options: {
permissionMode: "bypassPermissions", permissionMode: "bypassPermissions",
mcpServers, mcpServers,
+2 -2
View File
@@ -14,7 +14,7 @@ export const codex = agent({
executablePath: "bin/codex.js", executablePath: "bin/codex.js",
}); });
}, },
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { run: async ({ payload, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
process.env.OPENAI_API_KEY = apiKey; process.env.OPENAI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
@@ -39,7 +39,7 @@ export const codex = agent({
try { try {
// Use runStreamed to get streaming events similar to claude.ts // Use runStreamed to get streaming events similar to claude.ts
const streamedTurn = await thread.runStreamed(addInstructions(prompt)); const streamedTurn = await thread.runStreamed(addInstructions(payload));
// Stream events and handle them // Stream events and handle them
let finalOutput = ""; let finalOutput = "";
+2 -2
View File
@@ -14,7 +14,7 @@ export const cursor = agent({
executableName: "cursor-agent", executableName: "cursor-agent",
}); });
}, },
run: async ({ prompt, apiKey, cliPath, githubInstallationToken, mcpServers }) => { run: async ({ payload, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
process.env.CURSOR_API_KEY = apiKey; process.env.CURSOR_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
@@ -24,7 +24,7 @@ export const cursor = agent({
// Run cursor-agent in non-interactive mode with the prompt // Run cursor-agent in non-interactive mode with the prompt
// Using -p flag for prompt, --output-format text for plain text output // Using -p flag for prompt, --output-format text for plain text output
// and --approve-mcps to automatically approve all MCP servers // and --approve-mcps to automatically approve all MCP servers
const fullPrompt = addInstructions(prompt); const fullPrompt = addInstructions(payload);
// Find temp directory from cliPath to set HOME for MCP config lookup // Find temp directory from cliPath to set HOME for MCP config lookup
const tempDir = cliPath.split("/.local/bin/")[0]; const tempDir = cliPath.split("/.local/bin/")[0];
+3 -3
View File
@@ -15,7 +15,7 @@ export const gemini = agent({
installDependencies: true, installDependencies: true,
}); });
}, },
run: async ({ prompt, apiKey, mcpServers, githubInstallationToken, cliPath }) => { run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
configureGeminiMcpServers({ mcpServers, cliPath }); configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) { if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent"); throw new Error("google_api_key or gemini_api_key is required for gemini agent");
@@ -25,8 +25,8 @@ export const gemini = agent({
process.env.GEMINI_API_KEY = apiKey; process.env.GEMINI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
const sessionPrompt = addInstructions(prompt); const sessionPrompt = addInstructions(payload);
log.info(`Starting Gemini CLI with prompt: ${prompt.substring(0, 100)}...`); log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
let finalOutput = ""; let finalOutput = "";
try { try {
+12 -8
View File
@@ -1,13 +1,15 @@
import { ghPullfrogMcpName } from "../mcp/index.ts"; import { ghPullfrogMcpName } from "../mcp/index.ts";
import { modes } from "../modes.ts"; import { modes } from "../modes.ts";
import type { Payload } from "../payload.ts";
const userPromptHeader = `****** USER PROMPT ******\n`; // const userPromptHeader = `************* USER PROMPT *************\n`;
export const instructions = ` export const addInstructions = (payload: Payload) =>
`************* GENERAL INSTRUCTIONS *************
# General instructions # General instructions
You are a diligent, detail-oriented, no-nonsense software engineering agent. You are a diligent, detail-oriented, no-nonsense software engineering agent.
You will perform the task that is asked of you below ${userPromptHeader}. You will perform the task described in the *USER PROMPT* below.
You are careful, to-the-point, and kind. You only say things you know to be true. You are careful, to-the-point, and kind. You only say things you know to be true.
You have an extreme bias toward minimalism in your code and responses. You have an extreme bias toward minimalism in your code and responses.
Your code is focused, elegant, and production-ready. Your code is focused, elegant, and production-ready.
@@ -48,12 +50,14 @@ Ensure after your edits are done, your final comments do not contain intermediat
choose the appropriate mode based on the prompt payload: choose the appropriate mode based on the prompt payload:
${modes.map((w) => ` - "${w.name}": ${w.description}`).join("\n")} ${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
## Modes ## Modes
${modes.map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")} ${[...modes, ...payload.modes].map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")}
`;
export const addInstructions = (prompt: string) => ************* USER PROMPT *************
`****** GENERAL INSTRUCTIONS ******\n${instructions}\n\n${userPromptHeader}${prompt}`;
${payload.prompt}
${payload.event}`;
+2 -1
View File
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { pipeline } from "node:stream/promises"; import { pipeline } from "node:stream/promises";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { Payload } from "../payload.ts";
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
/** /**
@@ -23,7 +24,7 @@ export interface AgentResult {
export interface AgentConfig { export interface AgentConfig {
apiKey: string; apiKey: string;
githubInstallationToken: string; githubInstallationToken: string;
prompt: string; payload: Payload;
mcpServers: Record<string, McpStdioServerConfig>; mcpServers: Record<string, McpStdioServerConfig>;
cliPath: string; cliPath: string;
} }
+330 -385
View File
@@ -32913,12 +32913,13 @@ var package_default = {
}, },
dependencies: { dependencies: {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "0.1.37", "@anthropic-ai/claude-agent-sdk": "0.1.37",
"@openai/codex-sdk": "0.58.0",
"@ark/fs": "0.53.0", "@ark/fs": "0.53.0",
"@ark/util": "0.53.0", "@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"@openai/codex-sdk": "0.58.0",
"@standard-schema/spec": "1.0.0", "@standard-schema/spec": "1.0.0",
arktype: "2.1.25", arktype: "2.1.25",
dotenv: "^17.2.3", dotenv: "^17.2.3",
@@ -33214,13 +33215,11 @@ var modes = [
]; ];
// agents/instructions.ts // agents/instructions.ts
var userPromptHeader = `****** USER PROMPT ****** var addInstructions = (payload) => `************* GENERAL INSTRUCTIONS *************
`;
var instructions = `
# General instructions # General instructions
You are a diligent, detail-oriented, no-nonsense software engineering agent. You are a diligent, detail-oriented, no-nonsense software engineering agent.
You will perform the task that is asked of you below ${userPromptHeader}. You will perform the task described in the *USER PROMPT* below.
You are careful, to-the-point, and kind. You only say things you know to be true. You are careful, to-the-point, and kind. You only say things you know to be true.
You have an extreme bias toward minimalism in your code and responses. You have an extreme bias toward minimalism in your code and responses.
Your code is focused, elegant, and production-ready. Your code is focused, elegant, and production-ready.
@@ -33261,18 +33260,19 @@ Ensure after your edits are done, your final comments do not contain intermediat
choose the appropriate mode based on the prompt payload: choose the appropriate mode based on the prompt payload:
${modes.map((w) => ` - "${w.name}": ${w.description}`).join("\n")} ${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
## Modes ## Modes
${modes.map((w) => `### ${w.name} ${[...modes, ...payload.modes].map((w) => `### ${w.name}
${w.prompt}`).join("\n\n")} ${w.prompt}`).join("\n\n")}
`;
var addInstructions = (prompt) => `****** GENERAL INSTRUCTIONS ******
${instructions}
${userPromptHeader}${prompt}`; ************* USER PROMPT *************
${payload.prompt}
${payload.event}`;
// agents/shared.ts // agents/shared.ts
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
@@ -33284,7 +33284,8 @@ import { pipeline } from "node:stream/promises";
async function installFromNpmTarball({ async function installFromNpmTarball({
packageName, packageName,
version, version,
executablePath executablePath,
installDependencies
}) { }) {
let resolvedVersion = version; let resolvedVersion = version;
if (version.startsWith("^") || version.startsWith("~") || version === "latest") { if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
@@ -33342,6 +33343,20 @@ async function installFromNpmTarball({
if (!existsSync2(cliPath)) { if (!existsSync2(cliPath)) {
throw new Error(`Executable not found in extracted package at ${cliPath}`); throw new Error(`Executable not found in extracted package at ${cliPath}`);
} }
if (installDependencies) {
log.info(`Installing dependencies for ${packageName}...`);
const installResult = spawnSync("npm", ["install", "--production"], {
cwd: extractedDir,
stdio: "pipe",
encoding: "utf-8"
});
if (installResult.status !== 0) {
throw new Error(
`Failed to install dependencies: ${installResult.stderr || installResult.stdout || "Unknown error"}`
);
}
log.info(`\u2713 Dependencies installed`);
}
chmodSync(cliPath, 493); chmodSync(cliPath, 493);
log.info(`\u2713 ${packageName} installed at ${cliPath}`); log.info(`\u2713 ${packageName} installed at ${cliPath}`);
return cliPath; return cliPath;
@@ -33405,10 +33420,10 @@ var claude = agent({
executablePath: "cli.js" executablePath: "cli.js"
}); });
}, },
run: async ({ prompt, mcpServers, apiKey, cliPath }) => { run: async ({ payload, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey; process.env.ANTHROPIC_API_KEY = apiKey;
const queryInstance = query({ const queryInstance = query({
prompt: addInstructions(prompt), prompt: addInstructions(payload),
options: { options: {
permissionMode: "bypassPermissions", permissionMode: "bypassPermissions",
mcpServers, mcpServers,
@@ -33855,12 +33870,10 @@ var codex = agent({
executablePath: "bin/codex.js" executablePath: "bin/codex.js"
}); });
}, },
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { run: async ({ payload, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
process.env.OPENAI_API_KEY = apiKey; process.env.OPENAI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
if (mcpServers && Object.keys(mcpServers).length > 0) { configureCodexMcpServers({ mcpServers, cliPath });
configureMcpServers({ mcpServers, apiKey, cliPath });
}
const codexOptions = { const codexOptions = {
apiKey, apiKey,
codexPathOverride: cliPath codexPathOverride: cliPath
@@ -33872,7 +33885,7 @@ var codex = agent({
networkAccessEnabled: true networkAccessEnabled: true
}); });
try { try {
const streamedTurn = await thread.runStreamed(addInstructions(prompt)); const streamedTurn = await thread.runStreamed(addInstructions(payload));
let finalOutput = ""; let finalOutput = "";
for await (const event of streamedTurn.events) { for await (const event of streamedTurn.events) {
const handler = messageHandlers2[event.type]; const handler = messageHandlers2[event.type];
@@ -33968,17 +33981,8 @@ var messageHandlers2 = {
log.error(`Error: ${event.message}`); log.error(`Error: ${event.message}`);
} }
}; };
function configureMcpServers({ function configureCodexMcpServers({ mcpServers, cliPath }) {
mcpServers,
apiKey,
cliPath
}) {
log.info("Configuring MCP servers for Codex...");
for (const [serverName, serverConfig] of Object.entries(mcpServers)) { for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
const command = serverConfig.command; const command = serverConfig.command;
const args2 = serverConfig.args || []; const args2 = serverConfig.args || [];
const envVars = serverConfig.env || {}; const envVars = serverConfig.env || {};
@@ -33990,11 +33994,7 @@ function configureMcpServers({
log.info(`Adding MCP server '${serverName}'...`); log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync2("node", [cliPath, ...addArgs], { const addResult = spawnSync2("node", [cliPath, ...addArgs], {
stdio: "pipe", stdio: "pipe",
encoding: "utf-8", encoding: "utf-8"
env: {
...process.env,
OPENAI_API_KEY: apiKey
}
}); });
if (addResult.status !== 0) { if (addResult.status !== 0) {
throw new Error( throw new Error(
@@ -34011,27 +34011,25 @@ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:f
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
var cursor = agent({ var cursor = agent({
name: "cursor", name: "cursor",
inputKey: "cursor_api_key", inputKeys: ["cursor_api_key"],
install: async () => { install: async () => {
return await installFromCurl({ return await installFromCurl({
installUrl: "https://cursor.com/install", installUrl: "https://cursor.com/install",
executableName: "cursor-agent" executableName: "cursor-agent"
}); });
}, },
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { run: async ({ payload, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
process.env.CURSOR_API_KEY = apiKey; process.env.CURSOR_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
if (mcpServers && Object.keys(mcpServers).length > 0) { configureCursorMcpServers({ mcpServers, cliPath });
configureMcpServers2({ mcpServers, cliPath });
}
try { try {
const fullPrompt = addInstructions(prompt); const fullPrompt = addInstructions(payload);
const tempDir = cliPath.split("/.local/bin/")[0]; const tempDir = cliPath.split("/.local/bin/")[0];
log.info("Running Cursor CLI..."); log.info("Running Cursor CLI...");
return new Promise((resolve) => { return new Promise((resolve) => {
const child = spawn3( const child = spawn3(
cliPath, cliPath,
["--print", fullPrompt, "--output-format", "text", "--approve-mcps"], ["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"],
{ {
cwd: process.cwd(), cwd: process.cwd(),
// Run in current working directory // Run in current working directory
@@ -34048,37 +34046,31 @@ var cursor = agent({
); );
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
let hasOutput = false;
const timeout = setTimeout(() => {
if (!hasOutput && child.exitCode === null) {
log.warning("Cursor CLI appears to be hanging, killing process...");
child.kill("SIGTERM");
resolve({
success: false,
error: "Cursor CLI timed out - no output received",
output: stdout.trim()
});
}
}, 3e5);
child.on("spawn", () => { child.on("spawn", () => {
log.debug("Cursor CLI process spawned"); log.debug("Cursor CLI process spawned");
}); });
child.stdout?.on("data", (data) => { child.stdout?.on("data", (data) => {
hasOutput = true;
const text = data.toString(); const text = data.toString();
stdout += text; stdout += text;
process.stdout.write(text); process.stdout.write(text);
}); });
child.stderr?.on("data", (data) => { child.stderr?.on("data", (data) => {
hasOutput = true;
const text = data.toString(); const text = data.toString();
stderr += text; stderr += text;
process.stderr.write(text); process.stderr.write(text);
log.warning(text); log.warning(text);
}); });
child.on("close", (code) => { child.on("close", (code, signal) => {
clearTimeout(timeout); if (signal) {
if (code !== 0) { log.warning(`Cursor CLI terminated by signal: ${signal}`);
}
if (code === 0) {
log.success("Cursor CLI completed successfully");
resolve({
success: true,
output: stdout.trim()
});
} else {
const errorMessage = stderr || `Cursor CLI exited with code ${code}`; const errorMessage = stderr || `Cursor CLI exited with code ${code}`;
log.error(`Cursor CLI failed: ${errorMessage}`); log.error(`Cursor CLI failed: ${errorMessage}`);
resolve({ resolve({
@@ -34109,222 +34101,16 @@ var cursor = agent({
} }
} }
}); });
function configureMcpServers2({ function configureCursorMcpServers({ mcpServers, cliPath }) {
mcpServers,
cliPath
}) {
log.info("Configuring MCP servers for Cursor...");
const tempDir = cliPath.split("/.local/bin/")[0]; const tempDir = cliPath.split("/.local/bin/")[0];
const cursorConfigDir = join5(tempDir, ".cursor"); const cursorConfigDir = join5(tempDir, ".cursor");
const mcpConfigPath = join5(cursorConfigDir, "mcp.json"); const mcpConfigPath = join5(cursorConfigDir, "mcp.json");
const mcpConfig = {
mcpServers: {}
};
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
mcpConfig.mcpServers[serverName] = serverConfig;
log.info(`Adding MCP server '${serverName}'...`);
}
if (Object.keys(mcpConfig.mcpServers).length === 0) {
log.info("No MCP servers to configure");
return;
}
mkdirSync2(cursorConfigDir, { recursive: true }); mkdirSync2(cursorConfigDir, { recursive: true });
writeFileSync2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf-8"); writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
log.info(`\u2713 MCP configuration written to ${mcpConfigPath}`);
log.info("MCP servers configured. Cursor CLI will use --approve-mcps to auto-approve servers.");
} }
// utils/github.ts // agents/gemini.ts
var core2 = __toESM(require_core(), 1); import { spawnSync as spawnSync3 } from "node:child_process";
import { createSign } from "node:crypto";
function checkExistingToken() {
const inputToken = core2.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
return inputToken || envToken || null;
}
function isGitHubActionsEnvironment() {
return Boolean(process.env.GITHUB_ACTIONS);
}
async function acquireTokenViaOIDC() {
log.info("Generating OIDC token...");
const oidcToken = await core2.getIDToken("pullfrog-api");
log.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
log.info("Exchanging OIDC token for installation token...");
const timeoutMs = 5e3;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json"
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!tokenResponse.ok) {
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
}
const tokenData = await tokenResponse.json();
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token;
} catch (error2) {
clearTimeout(timeoutId);
if (error2 instanceof Error && error2.name === "AbortError") {
throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
}
throw error2;
}
}
var base64UrlEncode = (str) => {
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
};
var generateJWT = (appId, privateKey) => {
const now = Math.floor(Date.now() / 1e3);
const payload = {
iat: now - 60,
exp: now + 5 * 60,
iss: appId
};
const header = {
alg: "RS256",
typ: "JWT"
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signaturePart = `${encodedHeader}.${encodedPayload}`;
const signature = createSign("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return `${signaturePart}.${signature}`;
};
var githubRequest = async (path4, options = {}) => {
const { method = "GET", headers = {}, body } = options;
const url2 = `https://api.github.com${path4}`;
const requestHeaders = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
...headers
};
const response = await fetch(url2, {
method,
headers: requestHeaders,
...body && { body }
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`GitHub API request failed: ${response.status} ${response.statusText}
${errorText}`
);
}
return response.json();
};
var checkRepositoryAccess = async (token, repoOwner, repoName) => {
try {
const response = await githubRequest("/installation/repositories", {
headers: { Authorization: `token ${token}` }
});
return response.repositories.some(
(repo) => repo.owner.login === repoOwner && repo.name === repoName
);
} catch {
return false;
}
};
var createInstallationToken = async (jwt, installationId) => {
const response = await githubRequest(
`/app/installations/${installationId}/access_tokens`,
{
method: "POST",
headers: { Authorization: `Bearer ${jwt}` }
}
);
return response.token;
};
var findInstallationId = async (jwt, repoOwner, repoName) => {
const installations = await githubRequest("/app/installations", {
headers: { Authorization: `Bearer ${jwt}` }
});
for (const installation of installations) {
try {
const tempToken = await createInstallationToken(jwt, installation.id);
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
if (hasAccess) {
return installation.id;
}
} catch {
}
}
throw new Error(
`No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
);
};
async function acquireTokenViaGitHubApp() {
const repoContext = parseRepoContext();
const config = {
appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner,
repoName: repoContext.name
};
const jwt = generateJWT(config.appId, config.privateKey);
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
const token = await createInstallationToken(jwt, installationId);
return token;
}
async function acquireNewToken() {
if (isGitHubActionsEnvironment()) {
return await acquireTokenViaOIDC();
} else {
return await acquireTokenViaGitHubApp();
}
}
async function setupGitHubInstallationToken() {
const existingToken = checkExistingToken();
if (existingToken) {
core2.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return { githubInstallationToken: existingToken, wasAcquired: false };
}
const acquiredToken = await acquireNewToken();
core2.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
return { githubInstallationToken: acquiredToken, wasAcquired: true };
}
async function revokeInstallationToken(token) {
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
try {
await fetch(`${apiUrl}/installation/token`, {
method: "DELETE",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28"
}
});
log.info("Installation token revoked");
} catch (error2) {
log.warning(
`Failed to revoke installation token: ${error2 instanceof Error ? error2.message : String(error2)}`
);
}
}
function parseRepoContext() {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
throw new Error("GITHUB_REPOSITORY environment variable is required");
}
const [owner, name] = githubRepo.split("/");
if (!owner || !name) {
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
}
return { owner, name };
}
// utils/subprocess.ts // utils/subprocess.ts
import { spawn as nodeSpawn } from "node:child_process"; import { spawn as nodeSpawn } from "node:child_process";
@@ -34401,151 +34187,106 @@ async function spawn4(options) {
}); });
} }
// agents/jules.ts // agents/gemini.ts
var jules = agent({ var gemini = agent({
name: "jules", name: "gemini",
inputKeys: ["google_api_key", "gemini_api_key"], inputKeys: ["google_api_key", "gemini_api_key"],
install: async () => { install: async () => {
return await installFromNpmTarball({ return await installFromNpmTarball({
packageName: "@google/jules", packageName: "@google/gemini-cli",
version: "latest", version: "latest",
executablePath: "run.cjs" executablePath: "dist/index.js",
installDependencies: true
}); });
}, },
run: async ({ run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
prompt, configureGeminiMcpServers({ mcpServers, cliPath });
apiKey,
mcpServers: _mcpServers,
githubInstallationToken: _githubInstallationToken,
cliPath
}) => {
if (!apiKey) { if (!apiKey) {
throw new Error("google_api_key is required for jules agent"); throw new Error("google_api_key or gemini_api_key is required for gemini agent");
} }
process.env.GOOGLE_API_KEY = apiKey; process.env.GEMINI_API_KEY = apiKey;
const repoContext = parseRepoContext(); process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
const repoName = `${repoContext.owner}/${repoContext.name}`; const sessionPrompt = addInstructions(payload);
log.info(`Creating Jules session for ${repoName}...`); log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
const sessionPrompt = addInstructions(prompt);
log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`);
let sessionId;
try {
const createResult = await spawn4({
cmd: "node",
args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt],
onStdout: (chunk) => {
log.info(chunk.trim());
const match2 = chunk.match(/session[:\s]+(\d+)/i) || chunk.match(/id[:\s]+(\d+)/i);
if (match2 && !sessionId) {
sessionId = match2[1];
log.info(`\u2713 Session ID: ${sessionId}`);
}
},
onStderr: (chunk) => {
log.warning(chunk.trim());
}
});
if (createResult.exitCode !== 0) {
throw new Error(
`Failed to create Jules session: ${createResult.stderr || createResult.stdout || "Unknown error"}`
);
}
if (!sessionId) {
const output = createResult.stdout + createResult.stderr;
const match2 = output.match(/session[:\s]+(\d+)/i) || output.match(/id[:\s]+(\d+)/i);
if (match2) {
sessionId = match2[1];
}
}
if (!sessionId) {
log.warning("Could not extract session ID from output. Session may have been created.");
log.info(`Output: ${createResult.stdout}`);
} else {
log.info(`\u2713 Session created: ${sessionId}`);
}
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.error(`Failed to create Jules session: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: ""
};
}
log.info("Monitoring session progress...");
let finalOutput = ""; let finalOutput = "";
const maxPollAttempts = 300;
let pollAttempts = 0;
while (pollAttempts < maxPollAttempts) {
await new Promise((resolve) => setTimeout(resolve, 1e4));
pollAttempts++;
try { try {
const listResult = await spawn4({ const result = await spawn4({
cmd: "node", cmd: "node",
args: [cliPath, "remote", "list", "--session"], args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt],
env: {
GEMINI_API_KEY: apiKey,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken
},
onStdout: (chunk) => { onStdout: (chunk) => {
const trimmed = chunk.trim(); const trimmed = chunk.trim();
if (trimmed) { if (trimmed) {
log.info(trimmed); log.info(trimmed);
finalOutput += trimmed + "\n";
} }
}
});
if (listResult.exitCode === 0) {
const output = listResult.stdout;
if (sessionId && output.includes(sessionId)) {
if (output.includes("completed") || output.includes("done") || output.includes("finished")) {
log.info("Session appears to be completed");
finalOutput = "Session completed. Pulling results...";
break;
}
}
}
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.warning(`Error checking session status: ${errorMessage}`);
}
}
if (sessionId) {
try {
log.info(`Pulling results for session ${sessionId}...`);
const pullResult = await spawn4({
cmd: "node",
args: [cliPath, "remote", "pull", "--session", sessionId],
onStdout: (chunk) => {
log.info(chunk.trim());
}, },
onStderr: (chunk) => { onStderr: (chunk) => {
log.warning(chunk.trim()); const trimmed = chunk.trim();
if (trimmed) {
log.warning(trimmed);
finalOutput += trimmed + "\n";
}
} }
}); });
if (pullResult.exitCode === 0) { if (result.exitCode !== 0) {
finalOutput = pullResult.stdout || "Results pulled successfully."; const errorMessage = result.stderr || result.stdout || "Unknown error";
} else { log.error(`Gemini CLI exited with code ${result.exitCode}: ${errorMessage}`);
log.warning(`Failed to pull results: ${pullResult.stderr || pullResult.stdout}`);
finalOutput = finalOutput || "Session completed. Check Jules dashboard for results.";
}
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.warning(`Error pulling results: ${errorMessage}`);
}
}
if (pollAttempts >= maxPollAttempts) {
log.warning("Session monitoring timeout reached. Session may still be in progress.");
finalOutput = finalOutput || "Session monitoring timeout. Check Jules dashboard for session status.";
}
return { return {
success: true, success: false,
output: finalOutput || "Jules session completed. Check the Jules dashboard for results." error: errorMessage,
output: finalOutput || result.stdout || ""
}; };
} }
finalOutput = finalOutput || result.stdout || "Gemini CLI completed successfully.";
log.info("\u2713 Gemini CLI completed successfully");
return {
success: true,
output: finalOutput
};
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.error(`Failed to run Gemini CLI: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: finalOutput || ""
};
}
}
}); });
function configureGeminiMcpServers({ mcpServers, cliPath }) {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
const command = serverConfig.command;
const args2 = serverConfig.args || [];
const envVars = serverConfig.env || {};
const addArgs = ["mcp", "add", serverName, command, ...args2];
for (const [key, value2] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value2}`);
}
log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync3("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8"
});
if (addResult.status !== 0) {
throw new Error(
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
);
}
log.info(`\u2713 MCP server '${serverName}' configured`);
}
}
// agents/index.ts // agents/index.ts
var agents = { var agents = {
claude, claude,
codex, codex,
cursor, cursor,
jules gemini
}; };
// node_modules/.pnpm/@ark+schema@0.53.0/node_modules/@ark/schema/out/shared/registry.js // node_modules/.pnpm/@ark+schema@0.53.0/node_modules/@ark/schema/out/shared/registry.js
@@ -42292,6 +42033,194 @@ var dirOfCaller = () => dirname2(filePath(caller({ methodName: "dirOfCaller", up
var fromHere = (...joinWith) => join6(dirOfCaller(), ...joinWith); var fromHere = (...joinWith) => join6(dirOfCaller(), ...joinWith);
var fsRoot = parse(process3.cwd()).root; var fsRoot = parse(process3.cwd()).root;
// utils/github.ts
var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto";
function checkExistingToken() {
const inputToken = core2.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
return inputToken || envToken || null;
}
function isGitHubActionsEnvironment() {
return Boolean(process.env.GITHUB_ACTIONS);
}
async function acquireTokenViaOIDC() {
log.info("Generating OIDC token...");
const oidcToken = await core2.getIDToken("pullfrog-api");
log.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
log.info("Exchanging OIDC token for installation token...");
const timeoutMs = 5e3;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json"
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!tokenResponse.ok) {
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
}
const tokenData = await tokenResponse.json();
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token;
} catch (error2) {
clearTimeout(timeoutId);
if (error2 instanceof Error && error2.name === "AbortError") {
throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
}
throw error2;
}
}
var base64UrlEncode = (str) => {
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
};
var generateJWT = (appId, privateKey) => {
const now = Math.floor(Date.now() / 1e3);
const payload = {
iat: now - 60,
exp: now + 5 * 60,
iss: appId
};
const header = {
alg: "RS256",
typ: "JWT"
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signaturePart = `${encodedHeader}.${encodedPayload}`;
const signature = createSign("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return `${signaturePart}.${signature}`;
};
var githubRequest = async (path4, options = {}) => {
const { method = "GET", headers = {}, body } = options;
const url2 = `https://api.github.com${path4}`;
const requestHeaders = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
...headers
};
const response = await fetch(url2, {
method,
headers: requestHeaders,
...body && { body }
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`GitHub API request failed: ${response.status} ${response.statusText}
${errorText}`
);
}
return response.json();
};
var checkRepositoryAccess = async (token, repoOwner, repoName) => {
try {
const response = await githubRequest("/installation/repositories", {
headers: { Authorization: `token ${token}` }
});
return response.repositories.some(
(repo) => repo.owner.login === repoOwner && repo.name === repoName
);
} catch {
return false;
}
};
var createInstallationToken = async (jwt, installationId) => {
const response = await githubRequest(
`/app/installations/${installationId}/access_tokens`,
{
method: "POST",
headers: { Authorization: `Bearer ${jwt}` }
}
);
return response.token;
};
var findInstallationId = async (jwt, repoOwner, repoName) => {
const installations = await githubRequest("/app/installations", {
headers: { Authorization: `Bearer ${jwt}` }
});
for (const installation of installations) {
try {
const tempToken = await createInstallationToken(jwt, installation.id);
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
if (hasAccess) {
return installation.id;
}
} catch {
}
}
throw new Error(
`No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
);
};
async function acquireTokenViaGitHubApp() {
const repoContext = parseRepoContext();
const config = {
appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner,
repoName: repoContext.name
};
const jwt = generateJWT(config.appId, config.privateKey);
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
const token = await createInstallationToken(jwt, installationId);
return token;
}
async function acquireNewToken() {
if (isGitHubActionsEnvironment()) {
return await acquireTokenViaOIDC();
} else {
return await acquireTokenViaGitHubApp();
}
}
async function setupGitHubInstallationToken() {
const existingToken = checkExistingToken();
if (existingToken) {
core2.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return { githubInstallationToken: existingToken, wasAcquired: false };
}
const acquiredToken = await acquireNewToken();
core2.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
return { githubInstallationToken: acquiredToken, wasAcquired: true };
}
async function revokeInstallationToken(token) {
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
try {
await fetch(`${apiUrl}/installation/token`, {
method: "DELETE",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28"
}
});
log.info("Installation token revoked");
} catch (error2) {
log.warning(
`Failed to revoke installation token: ${error2 instanceof Error ? error2.message : String(error2)}`
);
}
}
function parseRepoContext() {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
throw new Error("GITHUB_REPOSITORY environment variable is required");
}
const [owner, name] = githubRepo.split("/");
if (!owner || !name) {
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
}
return { owner, name };
}
// mcp/config.ts // mcp/config.ts
function createMcpConfigs(githubInstallationToken) { function createMcpConfigs(githubInstallationToken) {
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
@@ -42460,7 +42389,23 @@ async function main(inputs) {
log.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(mcpServers, null, 2)}`); log.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
const cliPath = await agent2.install(); const cliPath = await agent2.install();
log.info(`Running ${agentName}...`); log.info(`Running ${agentName}...`);
log.box(inputs.prompt, { title: "Prompt" }); let payload;
try {
const parsedPrompt = JSON.parse(inputs.prompt);
if (!("~pullfrog" in parsedPrompt)) {
throw new Error("Invalid prompt: not a pullfrog webhook payload");
}
payload = parsedPrompt;
} catch {
payload = {
"~pullfrog": true,
agent: null,
prompt: inputs.prompt,
event: {},
modes
};
}
log.box(payload.prompt, { title: "Prompt" });
const matchingInputKey = agent2.inputKeys.find((inputKey) => inputs[inputKey]); const matchingInputKey = agent2.inputKeys.find((inputKey) => inputs[inputKey]);
if (!matchingInputKey) { if (!matchingInputKey) {
throwMissingApiKeyError({ throwMissingApiKeyError({
@@ -42472,7 +42417,7 @@ async function main(inputs) {
} }
const apiKey = inputs[matchingInputKey]; const apiKey = inputs[matchingInputKey];
const result = await agent2.run({ const result = await agent2.run({
prompt: inputs.prompt, payload,
mcpServers, mcpServers,
githubInstallationToken, githubInstallationToken,
apiKey, apiKey,
+23 -5
View File
@@ -2,7 +2,9 @@ import { flatMorph } from "@ark/util";
import { type } from "arktype"; import { type } from "arktype";
import { agents } from "./agents/index.ts"; import { agents } from "./agents/index.ts";
import { createMcpConfigs } from "./mcp/config.ts"; import { createMcpConfigs } from "./mcp/config.ts";
import { modes } from "./modes.ts";
import packageJson from "./package.json" with { type: "json" }; import packageJson from "./package.json" with { type: "json" };
import type { Payload } from "./payload.ts";
import { fetchRepoSettings } from "./utils/api.ts"; import { fetchRepoSettings } from "./utils/api.ts";
import { log } from "./utils/cli.ts"; import { log } from "./utils/cli.ts";
import { import {
@@ -122,11 +124,27 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const cliPath = await agent.install(); const cliPath = await agent.install();
log.info(`Running ${agentName}...`); log.info(`Running ${agentName}...`);
log.box(inputs.prompt, { title: "Prompt" });
// TODO: check if `inputs.prompts` is JSON let payload: Payload;
// if yes, check if it's a webhook payload or toJSON(github.event)
// for webhook payloads, check the specified `agent` field try {
// attempt JSON parsing
const parsedPrompt = JSON.parse(inputs.prompt);
if (!("~pullfrog" in parsedPrompt)) {
throw new Error("Invalid prompt: not a pullfrog webhook payload");
}
payload = parsedPrompt as Payload;
} catch {
payload = {
"~pullfrog": true,
agent: null,
prompt: inputs.prompt,
event: {},
modes,
};
}
log.box(payload.prompt, { title: "Prompt" });
const matchingInputKey = agent.inputKeys.find((inputKey) => inputs[inputKey]); const matchingInputKey = agent.inputKeys.find((inputKey) => inputs[inputKey]);
@@ -142,7 +160,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const apiKey = inputs[matchingInputKey]!; const apiKey = inputs[matchingInputKey]!;
const result = await agent.run({ const result = await agent.run({
prompt: inputs.prompt, payload,
mcpServers, mcpServers,
githubInstallationToken, githubInstallationToken,
apiKey, apiKey,
+56 -1
View File
@@ -97760,6 +97760,10 @@ var schema = ark.schema;
var define2 = ark.define; var define2 = ark.define;
var declare = ark.declare; var declare = ark.declare;
// mcp/shared.ts
import { appendFileSync } from "node:fs";
import { join } from "node:path";
// node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js // node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
function getUserAgent() { function getUserAgent() {
if (typeof navigator === "object" && "userAgent" in navigator) { if (typeof navigator === "object" && "userAgent" in navigator) {
@@ -101412,7 +101416,58 @@ var getMcpContext = cached2(() => {
}) })
}; };
}); });
var tool = (tool2) => tool2; function getLogPath() {
return join(process.cwd(), "log.txt");
}
function logToolCall({
toolName,
request: request2,
error: error41,
success
}) {
const logPath = getLogPath();
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const requestStr = JSON.stringify(request2, null, 2);
let logEntry = `[${timestamp}] Tool: ${toolName}
Request: ${requestStr}
`;
if (error41) {
const errorMessage = error41 instanceof Error ? error41.message : String(error41);
const errorStack = error41 instanceof Error ? error41.stack : void 0;
logEntry += `Error: ${errorMessage}
`;
if (errorStack) {
logEntry += `Stack: ${errorStack}
`;
}
logEntry += `Status: FAILED
`;
} else if (success !== void 0) {
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}
`;
}
logEntry += `${"=".repeat(80)}
`;
appendFileSync(logPath, logEntry, "utf-8");
}
var tool = (toolDef) => {
const toolName = toolDef.name;
const originalExecute = toolDef.execute;
toolDef.execute = async (args2, context) => {
try {
logToolCall({ toolName, request: args2 });
const result = await originalExecute(args2, context);
const isError = result && typeof result === "object" && "isError" in result && result.isError === true;
logToolCall({ toolName, request: args2, success: !isError });
return result;
} catch (error41) {
logToolCall({ toolName, request: args2, error: error41 });
throw error41;
}
};
return toolDef;
};
var addTools = (server2, tools) => { var addTools = (server2, tools) => {
for (const tool2 of tools) { for (const tool2 of tools) {
server2.addTool(tool2); server2.addTool(tool2);
+8 -2
View File
@@ -1,6 +1,12 @@
import { ghPullfrogMcpName } from "./mcp/index.ts"; import { ghPullfrogMcpName } from "./mcp/index.ts";
export const modes = [ export interface Mode {
name: string;
description: string;
prompt: string;
}
export const modes: Mode[] = [
{ {
name: "Plan", name: "Plan",
description: description:
@@ -51,4 +57,4 @@ export const modes = [
3. As your work progresses, update your Working Comment to share progress and results using ${ghPullfrogMcpName}/update_working_comment. Do not create additional comments unless you are explicitly asked to do so. 3. As your work progresses, update your Working Comment to share progress and results using ${ghPullfrogMcpName}/update_working_comment. Do not create additional comments unless you are explicitly asked to do so.
4. When you finish the task, update the Working Comment a final time with a summary of the results and links to any created issues, PRs, etc.`, 4. When you finish the task, update the Working Comment a final time with a summary of the results and links to any created issues, PRs, etc.`,
}, },
] as const; ];
+2 -1
View File
@@ -24,12 +24,13 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "0.1.37", "@anthropic-ai/claude-agent-sdk": "0.1.37",
"@openai/codex-sdk": "0.58.0",
"@ark/fs": "0.53.0", "@ark/fs": "0.53.0",
"@ark/util": "0.53.0", "@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1", "@octokit/webhooks-types": "^7.6.1",
"@openai/codex-sdk": "0.58.0",
"@standard-schema/spec": "1.0.0", "@standard-schema/spec": "1.0.0",
"arktype": "2.1.25", "arktype": "2.1.25",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
+37
View File
@@ -0,0 +1,37 @@
/** May be a `github.event` payload that has been stringified. This case needs to be detected and handled appropriately. */
import type { Mode } from "./modes.ts";
// type Payload = GithubEventPayload | WorkflowDispatchPayload;
// type GithubEventPayload = Record<string, any>;
export type Payload = {
"~pullfrog": true;
/**
* Agent slug identifier (e.g., "claude", "codex", "gemini")
*/
readonly agent: string | null;
/**
* The prompt/instructions for the agent to execute
*/
readonly prompt: string;
/**
* Event data from webhook payload.
*/
readonly event: object;
/**
* Execution mode configuration
*/
modes: readonly Mode[];
/**
* Optional IDs of the issue, PR, or comment that the agent is working on
*/
readonly comment_id?: number | null;
readonly issue_id?: number | null;
readonly pr_id?: number | null;
};
+137
View File
@@ -11,6 +11,9 @@ importers:
'@actions/core': '@actions/core':
specifier: ^1.11.1 specifier: ^1.11.1
version: 1.11.1 version: 1.11.1
'@actions/github':
specifier: ^6.0.1
version: 6.0.1
'@anthropic-ai/claude-agent-sdk': '@anthropic-ai/claude-agent-sdk':
specifier: 0.1.37 specifier: 0.1.37
version: 0.1.37(zod@3.25.76) version: 0.1.37(zod@3.25.76)
@@ -75,6 +78,9 @@ packages:
'@actions/exec@1.1.1': '@actions/exec@1.1.1':
resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==} resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==}
'@actions/github@6.0.1':
resolution: {integrity: sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==}
'@actions/http-client@2.2.3': '@actions/http-client@2.2.3':
resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==} resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
@@ -324,10 +330,18 @@ packages:
resolution: {integrity: sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==} resolution: {integrity: sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@octokit/auth-token@4.0.0':
resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
engines: {node: '>= 18'}
'@octokit/auth-token@6.0.0': '@octokit/auth-token@6.0.0':
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@octokit/core@5.2.2':
resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==}
engines: {node: '>= 18'}
'@octokit/core@7.0.5': '@octokit/core@7.0.5':
resolution: {integrity: sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==} resolution: {integrity: sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
@@ -336,10 +350,24 @@ packages:
resolution: {integrity: sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==} resolution: {integrity: sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@octokit/endpoint@9.0.6':
resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==}
engines: {node: '>= 18'}
'@octokit/graphql@7.1.1':
resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==}
engines: {node: '>= 18'}
'@octokit/graphql@9.0.2': '@octokit/graphql@9.0.2':
resolution: {integrity: sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==} resolution: {integrity: sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@octokit/openapi-types@20.0.0':
resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==}
'@octokit/openapi-types@24.2.0':
resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==}
'@octokit/openapi-types@26.0.0': '@octokit/openapi-types@26.0.0':
resolution: {integrity: sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==} resolution: {integrity: sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==}
@@ -349,18 +377,34 @@ packages:
peerDependencies: peerDependencies:
'@octokit/core': '>=6' '@octokit/core': '>=6'
'@octokit/plugin-paginate-rest@9.2.2':
resolution: {integrity: sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '5'
'@octokit/plugin-request-log@6.0.0': '@octokit/plugin-request-log@6.0.0':
resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
peerDependencies: peerDependencies:
'@octokit/core': '>=6' '@octokit/core': '>=6'
'@octokit/plugin-rest-endpoint-methods@10.4.1':
resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '5'
'@octokit/plugin-rest-endpoint-methods@16.1.0': '@octokit/plugin-rest-endpoint-methods@16.1.0':
resolution: {integrity: sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==} resolution: {integrity: sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
peerDependencies: peerDependencies:
'@octokit/core': '>=6' '@octokit/core': '>=6'
'@octokit/request-error@5.1.1':
resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==}
engines: {node: '>= 18'}
'@octokit/request-error@7.0.1': '@octokit/request-error@7.0.1':
resolution: {integrity: sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==} resolution: {integrity: sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
@@ -369,10 +413,20 @@ packages:
resolution: {integrity: sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==} resolution: {integrity: sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@octokit/request@8.4.1':
resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==}
engines: {node: '>= 18'}
'@octokit/rest@22.0.0': '@octokit/rest@22.0.0':
resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==} resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@octokit/types@12.6.0':
resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==}
'@octokit/types@13.10.0':
resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
'@octokit/types@15.0.0': '@octokit/types@15.0.0':
resolution: {integrity: sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==} resolution: {integrity: sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==}
@@ -442,6 +496,9 @@ packages:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
before-after-hook@4.0.0: before-after-hook@4.0.0:
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
@@ -509,6 +566,9 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
deprecation@2.3.1:
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
dotenv@17.2.3: dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -953,6 +1013,9 @@ packages:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'} engines: {node: '>=18'}
universal-user-agent@6.0.1:
resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
universal-user-agent@7.0.3: universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
@@ -1040,6 +1103,16 @@ snapshots:
dependencies: dependencies:
'@actions/io': 1.1.3 '@actions/io': 1.1.3
'@actions/github@6.0.1':
dependencies:
'@actions/http-client': 2.2.3
'@octokit/core': 5.2.2
'@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.2)
'@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.2)
'@octokit/request': 8.4.1
'@octokit/request-error': 5.1.1
undici: 5.29.0
'@actions/http-client@2.2.3': '@actions/http-client@2.2.3':
dependencies: dependencies:
tunnel: 0.0.6 tunnel: 0.0.6
@@ -1208,8 +1281,20 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@octokit/auth-token@4.0.0': {}
'@octokit/auth-token@6.0.0': {} '@octokit/auth-token@6.0.0': {}
'@octokit/core@5.2.2':
dependencies:
'@octokit/auth-token': 4.0.0
'@octokit/graphql': 7.1.1
'@octokit/request': 8.4.1
'@octokit/request-error': 5.1.1
'@octokit/types': 13.10.0
before-after-hook: 2.2.3
universal-user-agent: 6.0.1
'@octokit/core@7.0.5': '@octokit/core@7.0.5':
dependencies: dependencies:
'@octokit/auth-token': 6.0.0 '@octokit/auth-token': 6.0.0
@@ -1225,12 +1310,27 @@ snapshots:
'@octokit/types': 15.0.0 '@octokit/types': 15.0.0
universal-user-agent: 7.0.3 universal-user-agent: 7.0.3
'@octokit/endpoint@9.0.6':
dependencies:
'@octokit/types': 13.10.0
universal-user-agent: 6.0.1
'@octokit/graphql@7.1.1':
dependencies:
'@octokit/request': 8.4.1
'@octokit/types': 13.10.0
universal-user-agent: 6.0.1
'@octokit/graphql@9.0.2': '@octokit/graphql@9.0.2':
dependencies: dependencies:
'@octokit/request': 10.0.5 '@octokit/request': 10.0.5
'@octokit/types': 15.0.0 '@octokit/types': 15.0.0
universal-user-agent: 7.0.3 universal-user-agent: 7.0.3
'@octokit/openapi-types@20.0.0': {}
'@octokit/openapi-types@24.2.0': {}
'@octokit/openapi-types@26.0.0': {} '@octokit/openapi-types@26.0.0': {}
'@octokit/plugin-paginate-rest@13.2.0(@octokit/core@7.0.5)': '@octokit/plugin-paginate-rest@13.2.0(@octokit/core@7.0.5)':
@@ -1238,15 +1338,31 @@ snapshots:
'@octokit/core': 7.0.5 '@octokit/core': 7.0.5
'@octokit/types': 15.0.0 '@octokit/types': 15.0.0
'@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2)':
dependencies:
'@octokit/core': 5.2.2
'@octokit/types': 12.6.0
'@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.5)': '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.5)':
dependencies: dependencies:
'@octokit/core': 7.0.5 '@octokit/core': 7.0.5
'@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.2)':
dependencies:
'@octokit/core': 5.2.2
'@octokit/types': 12.6.0
'@octokit/plugin-rest-endpoint-methods@16.1.0(@octokit/core@7.0.5)': '@octokit/plugin-rest-endpoint-methods@16.1.0(@octokit/core@7.0.5)':
dependencies: dependencies:
'@octokit/core': 7.0.5 '@octokit/core': 7.0.5
'@octokit/types': 15.0.0 '@octokit/types': 15.0.0
'@octokit/request-error@5.1.1':
dependencies:
'@octokit/types': 13.10.0
deprecation: 2.3.1
once: 1.4.0
'@octokit/request-error@7.0.1': '@octokit/request-error@7.0.1':
dependencies: dependencies:
'@octokit/types': 15.0.0 '@octokit/types': 15.0.0
@@ -1259,6 +1375,13 @@ snapshots:
fast-content-type-parse: 3.0.0 fast-content-type-parse: 3.0.0
universal-user-agent: 7.0.3 universal-user-agent: 7.0.3
'@octokit/request@8.4.1':
dependencies:
'@octokit/endpoint': 9.0.6
'@octokit/request-error': 5.1.1
'@octokit/types': 13.10.0
universal-user-agent: 6.0.1
'@octokit/rest@22.0.0': '@octokit/rest@22.0.0':
dependencies: dependencies:
'@octokit/core': 7.0.5 '@octokit/core': 7.0.5
@@ -1266,6 +1389,14 @@ snapshots:
'@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.5) '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.5)
'@octokit/plugin-rest-endpoint-methods': 16.1.0(@octokit/core@7.0.5) '@octokit/plugin-rest-endpoint-methods': 16.1.0(@octokit/core@7.0.5)
'@octokit/types@12.6.0':
dependencies:
'@octokit/openapi-types': 20.0.0
'@octokit/types@13.10.0':
dependencies:
'@octokit/openapi-types': 24.2.0
'@octokit/types@15.0.0': '@octokit/types@15.0.0':
dependencies: dependencies:
'@octokit/openapi-types': 26.0.0 '@octokit/openapi-types': 26.0.0
@@ -1337,6 +1468,8 @@ snapshots:
astral-regex@2.0.0: {} astral-regex@2.0.0: {}
before-after-hook@2.2.3: {}
before-after-hook@4.0.0: {} before-after-hook@4.0.0: {}
body-parser@2.2.0: body-parser@2.2.0:
@@ -1404,6 +1537,8 @@ snapshots:
depd@2.0.0: {} depd@2.0.0: {}
deprecation@2.3.1: {}
dotenv@17.2.3: {} dotenv@17.2.3: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
@@ -1889,6 +2024,8 @@ snapshots:
unicorn-magic@0.3.0: {} unicorn-magic@0.3.0: {}
universal-user-agent@6.0.1: {}
universal-user-agent@7.0.3: {} universal-user-agent@7.0.3: {}
unpipe@1.0.0: {} unpipe@1.0.0: {}