init
This commit is contained in:
committed by
Colin McDonnell
parent
2f3ae3e481
commit
c335032c37
@@ -84,6 +84,7 @@ on:
|
|||||||
prompt:
|
prompt:
|
||||||
description: 'Agent prompt'
|
description: 'Agent prompt'
|
||||||
type: string
|
type: string
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -105,14 +106,14 @@ jobs:
|
|||||||
- name: Run agent
|
- name: Run agent
|
||||||
uses: pullfrog/action@v0
|
uses: pullfrog/action@v0
|
||||||
with:
|
with:
|
||||||
prompt: ${{ github.event.inputs.prompt }}
|
prompt: ${{ inputs.prompt }}
|
||||||
|
env:
|
||||||
# feel free to comment out any you won't use
|
# feel free to comment out any you won't use
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
google_api_key: ${{ secrets.GOOGLE_API_KEY }}
|
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||||
gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
cursor_api_key: ${{ secrets.CURSOR_API_KEY }}
|
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+7
-19
@@ -1,30 +1,18 @@
|
|||||||
name: "Pullfrog Claude Code Action"
|
name: "Pullfrog Action"
|
||||||
description: "Execute Claude Code with a prompt using Anthropic API"
|
description: "Execute coding agents with a prompt"
|
||||||
author: "Pullfrog"
|
author: "Pullfrog"
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
prompt:
|
prompt:
|
||||||
description: "Prompt to send to Claude Code"
|
description: "Prompt to send to the agent"
|
||||||
required: true
|
required: true
|
||||||
default: "Hello from Claude Code!"
|
effort:
|
||||||
anthropic_api_key:
|
description: "Effort level: nothink (fast), think (default), max (most capable)"
|
||||||
description: "Anthropic API key for Claude Code authentication"
|
|
||||||
required: false
|
|
||||||
openai_api_key:
|
|
||||||
description: "OpenAI API key for Codex authentication"
|
|
||||||
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
|
required: false
|
||||||
|
default: "think"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "node20"
|
using: "node24"
|
||||||
main: "entry"
|
main: "entry"
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
|
|||||||
+28
-8
@@ -1,9 +1,18 @@
|
|||||||
import { type Options, query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
import { type Options, query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import type { Effort } from "../external.ts";
|
||||||
import packageJson from "../package.json" with { type: "json" };
|
import packageJson from "../package.json" with { type: "json" };
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { addInstructions } from "./instructions.ts";
|
import { addInstructions } from "./instructions.ts";
|
||||||
import { agent, createAgentEnv, installFromNpmTarball } from "./shared.ts";
|
import { agent, createAgentEnv, installFromNpmTarball } from "./shared.ts";
|
||||||
|
|
||||||
|
// model configuration based on effort level
|
||||||
|
// uses model family aliases that auto-resolve to latest version
|
||||||
|
const claudeModels: Record<Effort, { model: string; thinking: boolean }> = {
|
||||||
|
nothink: { model: "claude-haiku-4-5", thinking: false },
|
||||||
|
think: { model: "claude-sonnet-4-5", thinking: true },
|
||||||
|
max: { model: "claude-opus-4-5", thinking: true },
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const claude = agent({
|
export const claude = agent({
|
||||||
name: "claude",
|
name: "claude",
|
||||||
install: async () => {
|
install: async () => {
|
||||||
@@ -14,13 +23,17 @@ export const claude = agent({
|
|||||||
executablePath: "cli.js",
|
executablePath: "cli.js",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
run: async ({ payload, mcpServers, apiKey, cliPath, repo }) => {
|
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort }) => {
|
||||||
// Ensure API key is NOT in process.env - only pass via SDK's env option
|
// Ensure API key is NOT in process.env - only pass via SDK's env option
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
delete process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
const prompt = addInstructions({ payload, repo });
|
const prompt = addInstructions({ payload, repo });
|
||||||
log.group("Full prompt", () => log.info(prompt));
|
log.group("Full prompt", () => log.info(prompt));
|
||||||
|
|
||||||
|
// get model configuration based on effort
|
||||||
|
const modelConfig = claudeModels[effort];
|
||||||
|
log.info(`Using model: ${modelConfig.model}, thinking: ${modelConfig.thinking}`);
|
||||||
|
|
||||||
// SECURITY: For PUBLIC repos, Claude Code spawns subprocesses with full process.env, leaking API keys.
|
// SECURITY: For PUBLIC repos, Claude Code spawns subprocesses with full process.env, leaking API keys.
|
||||||
// disable native Bash; agents use MCP bash tool which filters secrets.
|
// disable native Bash; agents use MCP bash tool which filters secrets.
|
||||||
// for private repos, native Bash is allowed since secrets are less exposed.
|
// for private repos, native Bash is allowed since secrets are less exposed.
|
||||||
@@ -46,15 +59,22 @@ export const claude = agent({
|
|||||||
|
|
||||||
// Pass secrets via SDK's env option only (not process.env)
|
// Pass secrets via SDK's env option only (not process.env)
|
||||||
// This ensures secrets are only available to Claude Code subprocess, not user code
|
// This ensures secrets are only available to Claude Code subprocess, not user code
|
||||||
|
const queryOptions: Options = {
|
||||||
|
...sandboxOptions,
|
||||||
|
mcpServers,
|
||||||
|
model: modelConfig.model,
|
||||||
|
pathToClaudeCodeExecutable: cliPath,
|
||||||
|
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }),
|
||||||
|
};
|
||||||
|
// only set maxThinkingTokens when we want to disable thinking (0)
|
||||||
|
// omit the property to use default when thinking is enabled
|
||||||
|
if (!modelConfig.thinking) {
|
||||||
|
queryOptions.maxThinkingTokens = 0;
|
||||||
|
}
|
||||||
|
|
||||||
const queryInstance = query({
|
const queryInstance = query({
|
||||||
prompt,
|
prompt,
|
||||||
options: {
|
options: queryOptions,
|
||||||
...sandboxOptions,
|
|
||||||
mcpServers,
|
|
||||||
// model: "claude-opus-4-5",
|
|
||||||
pathToClaudeCodeExecutable: cliPath,
|
|
||||||
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream the results
|
// Stream the results
|
||||||
|
|||||||
+16
-1
@@ -2,10 +2,19 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
|
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
|
||||||
|
import type { Effort } from "../external.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { addInstructions } from "./instructions.ts";
|
import { addInstructions } from "./instructions.ts";
|
||||||
import { agent, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts";
|
import { agent, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts";
|
||||||
|
|
||||||
|
// model configuration based on effort level
|
||||||
|
// uses model family aliases that auto-resolve to latest version
|
||||||
|
const codexModels: Record<Effort, string> = {
|
||||||
|
nothink: "gpt-4o-mini",
|
||||||
|
think: "gpt-5.2-instant",
|
||||||
|
max: "gpt-5.2-thinking",
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface WriteCodexConfigParams {
|
interface WriteCodexConfigParams {
|
||||||
tempHome: string;
|
tempHome: string;
|
||||||
mcpServers: Record<string, McpHttpServerConfig>;
|
mcpServers: Record<string, McpHttpServerConfig>;
|
||||||
@@ -60,7 +69,7 @@ export const codex = agent({
|
|||||||
executablePath: "bin/codex.js",
|
executablePath: "bin/codex.js",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
run: async ({ payload, mcpServers, apiKey, cliPath, repo }) => {
|
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort }) => {
|
||||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
|
||||||
// create config directory for codex before setting HOME
|
// create config directory for codex before setting HOME
|
||||||
@@ -79,6 +88,10 @@ export const codex = agent({
|
|||||||
CODEX_HOME: codexDir, // point Codex to our config directory
|
CODEX_HOME: codexDir, // point Codex to our config directory
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// get model based on effort level
|
||||||
|
const model = codexModels[effort];
|
||||||
|
log.info(`Using model: ${model}`);
|
||||||
|
|
||||||
// Configure Codex
|
// Configure Codex
|
||||||
const codexOptions: CodexOptions = {
|
const codexOptions: CodexOptions = {
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -93,11 +106,13 @@ export const codex = agent({
|
|||||||
const thread = codex.startThread(
|
const thread = codex.startThread(
|
||||||
payload.sandbox
|
payload.sandbox
|
||||||
? {
|
? {
|
||||||
|
model,
|
||||||
approvalPolicy: "never",
|
approvalPolicy: "never",
|
||||||
sandboxMode: "read-only",
|
sandboxMode: "read-only",
|
||||||
networkAccessEnabled: false,
|
networkAccessEnabled: false,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
model,
|
||||||
approvalPolicy: "never",
|
approvalPolicy: "never",
|
||||||
// use danger-full-access to allow git operations (workspace-write blocks .git directory writes)
|
// use danger-full-access to allow git operations (workspace-write blocks .git directory writes)
|
||||||
sandboxMode: "danger-full-access",
|
sandboxMode: "danger-full-access",
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ export const cursor = agent({
|
|||||||
executableName: "cursor-agent",
|
executableName: "cursor-agent",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
run: async ({ payload, apiKey, cliPath, mcpServers, repo }) => {
|
run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort: _effort }) => {
|
||||||
configureCursorMcpServers({ mcpServers, cliPath });
|
configureCursorMcpServers({ mcpServers, cliPath });
|
||||||
configureCursorSandbox({ sandbox: payload.sandbox ?? false, isPublicRepo: repo.isPublic });
|
configureCursorSandbox({ sandbox: payload.sandbox ?? false, isPublicRepo: repo.isPublic });
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -11,6 +11,14 @@ import {
|
|||||||
installFromGithub,
|
installFromGithub,
|
||||||
} from "./shared.ts";
|
} from "./shared.ts";
|
||||||
|
|
||||||
|
// model configuration based on effort level
|
||||||
|
// uses model family aliases that auto-resolve to latest version
|
||||||
|
const geminiModels = {
|
||||||
|
nothink: "gemini-2.5-pro",
|
||||||
|
think: "gemini-3-pro",
|
||||||
|
max: "gemini-3-pro",
|
||||||
|
} as const;
|
||||||
|
|
||||||
// gemini cli event types inferred from stream-json output (NDJSON format)
|
// gemini cli event types inferred from stream-json output (NDJSON format)
|
||||||
interface GeminiInitEvent {
|
interface GeminiInitEvent {
|
||||||
type: "init";
|
type: "init";
|
||||||
@@ -156,13 +164,17 @@ export const gemini = agent({
|
|||||||
...(githubInstallationToken && { githubInstallationToken }),
|
...(githubInstallationToken && { githubInstallationToken }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
run: async ({ payload, apiKey, mcpServers, cliPath, repo }) => {
|
run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort }) => {
|
||||||
configureGeminiMcpServers({ mcpServers, isPublicRepo: repo.isPublic });
|
configureGeminiMcpServers({ mcpServers, isPublicRepo: repo.isPublic });
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get model based on effort level
|
||||||
|
const model = geminiModels[effort];
|
||||||
|
log.info(`Using model: ${model}`);
|
||||||
|
|
||||||
const sessionPrompt = addInstructions({ payload, repo });
|
const sessionPrompt = addInstructions({ payload, repo });
|
||||||
log.group("Full prompt", () => log.info(sessionPrompt));
|
log.group("Full prompt", () => log.info(sessionPrompt));
|
||||||
|
|
||||||
@@ -172,6 +184,8 @@ export const gemini = agent({
|
|||||||
if (payload.sandbox) {
|
if (payload.sandbox) {
|
||||||
// sandbox mode: read-only tools only
|
// sandbox mode: read-only tools only
|
||||||
args = [
|
args = [
|
||||||
|
"--model",
|
||||||
|
model,
|
||||||
"--allowed-tools",
|
"--allowed-tools",
|
||||||
"read_file,list_directory,search_file_content,glob,save_memory,write_todos",
|
"read_file,list_directory,search_file_content,glob,save_memory,write_todos",
|
||||||
"--allowed-mcp-server-names",
|
"--allowed-mcp-server-names",
|
||||||
@@ -183,7 +197,7 @@ export const gemini = agent({
|
|||||||
} else {
|
} else {
|
||||||
// normal mode: --yolo for auto-approval
|
// normal mode: --yolo for auto-approval
|
||||||
// for public repos, shell is excluded via settings.json excludeTools
|
// for public repos, shell is excluded via settings.json excludeTools
|
||||||
args = ["--yolo", "--output-format=stream-json", "-p", sessionPrompt];
|
args = ["--model", model, "--yolo", "--output-format=stream-json", "-p", sessionPrompt];
|
||||||
if (repo.isPublic) {
|
if (repo.isPublic) {
|
||||||
log.info("🔒 public repo: native shell disabled via excludeTools, using MCP bash");
|
log.info("🔒 public repo: native shell disabled via excludeTools, using MCP bash");
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -22,7 +22,15 @@ export const opencode = agent({
|
|||||||
installDependencies: true,
|
installDependencies: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, repo }) => {
|
run: async ({
|
||||||
|
payload,
|
||||||
|
apiKey: _apiKey,
|
||||||
|
apiKeys,
|
||||||
|
mcpServers,
|
||||||
|
cliPath,
|
||||||
|
repo,
|
||||||
|
effort: _effort,
|
||||||
|
}) => {
|
||||||
// 1. configure home/config directory
|
// 1. configure home/config directory
|
||||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||||
const configDir = join(tempHome, ".config", "opencode");
|
const configDir = join(tempHome, ".config", "opencode");
|
||||||
@@ -60,10 +68,9 @@ export const opencode = agent({
|
|||||||
|
|
||||||
// add API keys from apiKeys object
|
// add API keys from apiKeys object
|
||||||
for (const [key, value] of Object.entries(apiKeys || {})) {
|
for (const [key, value] of Object.entries(apiKeys || {})) {
|
||||||
const upperKey = key.toUpperCase();
|
env[key] = value;
|
||||||
env[upperKey] = value;
|
|
||||||
// also set GOOGLE_GENERATIVE_AI_API_KEY for Google provider compatibility
|
// also set GOOGLE_GENERATIVE_AI_API_KEY for Google provider compatibility
|
||||||
if (upperKey === "GEMINI_API_KEY") {
|
if (key === "GEMINI_API_KEY") {
|
||||||
env.GOOGLE_GENERATIVE_AI_API_KEY = value;
|
env.GOOGLE_GENERATIVE_AI_API_KEY = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -6,7 +6,13 @@ import { join } from "node:path";
|
|||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import type { show } from "@ark/util";
|
import type { show } from "@ark/util";
|
||||||
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
import {
|
||||||
|
type AgentManifest,
|
||||||
|
type AgentName,
|
||||||
|
agentsManifest,
|
||||||
|
type Effort,
|
||||||
|
type Payload,
|
||||||
|
} from "../external.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { getGitHubInstallationToken } from "../utils/github.ts";
|
import { getGitHubInstallationToken } from "../utils/github.ts";
|
||||||
|
|
||||||
@@ -40,6 +46,7 @@ export interface AgentConfig {
|
|||||||
mcpServers: Record<string, McpHttpServerConfig>;
|
mcpServers: Record<string, McpHttpServerConfig>;
|
||||||
cliPath: string;
|
cliPath: string;
|
||||||
repo: RepoInfo;
|
repo: RepoInfo;
|
||||||
|
effort: Effort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { flatMorph } from "@ark/util";
|
import { Inputs, main } from "./main.ts";
|
||||||
import { agents } from "./agents/index.ts";
|
|
||||||
import { type Inputs, main } from "./main.ts";
|
|
||||||
import { log } from "./utils/cli.ts";
|
import { log } from "./utils/cli.ts";
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
@@ -20,12 +18,10 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const inputs: Required<Inputs> = {
|
const inputs = Inputs.assert({
|
||||||
prompt: core.getInput("prompt", { required: true }),
|
prompt: core.getInput("prompt", { required: true }),
|
||||||
...flatMorph(agents, (_, agent) =>
|
effort: core.getInput("effort") || "think",
|
||||||
agent.apiKeyNames.map((inputKey) => [inputKey, core.getInput(inputKey)])
|
});
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await main(inputs);
|
const result = await main(inputs);
|
||||||
|
|
||||||
|
|||||||
+14
-4
@@ -20,22 +20,22 @@ export interface AgentManifest {
|
|||||||
export const agentsManifest = {
|
export const agentsManifest = {
|
||||||
claude: {
|
claude: {
|
||||||
displayName: "Claude Code",
|
displayName: "Claude Code",
|
||||||
apiKeyNames: ["anthropic_api_key"],
|
apiKeyNames: ["ANTHROPIC_API_KEY"],
|
||||||
url: "https://claude.com/claude-code",
|
url: "https://claude.com/claude-code",
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
displayName: "Codex CLI",
|
displayName: "Codex CLI",
|
||||||
apiKeyNames: ["openai_api_key"],
|
apiKeyNames: ["OPENAI_API_KEY"],
|
||||||
url: "https://platform.openai.com/docs/guides/codex",
|
url: "https://platform.openai.com/docs/guides/codex",
|
||||||
},
|
},
|
||||||
cursor: {
|
cursor: {
|
||||||
displayName: "Cursor CLI",
|
displayName: "Cursor CLI",
|
||||||
apiKeyNames: ["cursor_api_key"],
|
apiKeyNames: ["CURSOR_API_KEY"],
|
||||||
url: "https://cursor.com/",
|
url: "https://cursor.com/",
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
displayName: "Gemini CLI",
|
displayName: "Gemini CLI",
|
||||||
apiKeyNames: ["google_api_key", "gemini_api_key"],
|
apiKeyNames: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
||||||
url: "https://ai.google.dev/gemini-api/docs",
|
url: "https://ai.google.dev/gemini-api/docs",
|
||||||
},
|
},
|
||||||
opencode: {
|
opencode: {
|
||||||
@@ -51,6 +51,10 @@ export const AgentName = type.enumerated(...Object.keys(agentsManifest));
|
|||||||
|
|
||||||
export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number];
|
export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number];
|
||||||
|
|
||||||
|
// effort level type - controls model selection and thinking level
|
||||||
|
export const Effort = type.enumerated("nothink", "think", "max");
|
||||||
|
export type Effort = typeof Effort.infer;
|
||||||
|
|
||||||
// base interface for common payload event fields
|
// base interface for common payload event fields
|
||||||
interface BasePayloadEvent {
|
interface BasePayloadEvent {
|
||||||
issue_number?: number;
|
issue_number?: number;
|
||||||
@@ -279,6 +283,12 @@ export interface Payload extends DispatchOptions {
|
|||||||
*/
|
*/
|
||||||
modes: readonly Mode[];
|
modes: readonly Mode[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effort level for model selection (nothink, think, max)
|
||||||
|
* Defaults to "think" if not specified
|
||||||
|
*/
|
||||||
|
readonly effort?: Effort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional IDs of the issue, PR, or comment that the agent is working on
|
* Optional IDs of the issue, PR, or comment that the agent is working on
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { encode as toonEncode } from "@toon-format/toon";
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { type Agent, agents } from "./agents/index.ts";
|
import { type Agent, agents } from "./agents/index.ts";
|
||||||
import type { AgentResult } from "./agents/shared.ts";
|
import type { AgentResult } from "./agents/shared.ts";
|
||||||
import type { AgentName, Payload } from "./external.ts";
|
import { type AgentName, agentsManifest, Effort, type Payload } from "./external.ts";
|
||||||
import { agentsManifest } from "./external.ts";
|
|
||||||
import { ensureProgressCommentUpdated, reportProgress } from "./mcp/comment.ts";
|
import { ensureProgressCommentUpdated, reportProgress } from "./mcp/comment.ts";
|
||||||
import { createMcpConfigs } from "./mcp/config.ts";
|
import { createMcpConfigs } from "./mcp/config.ts";
|
||||||
import { startMcpHttpServer } from "./mcp/server.ts";
|
import { startMcpHttpServer } from "./mcp/server.ts";
|
||||||
@@ -18,29 +17,13 @@ import type { PrepResult } from "./prep/index.ts";
|
|||||||
import { fetchRepoSettings, fetchWorkflowRunInfo, type RepoSettings } from "./utils/api.ts";
|
import { fetchRepoSettings, fetchWorkflowRunInfo, type RepoSettings } from "./utils/api.ts";
|
||||||
import { log } from "./utils/cli.ts";
|
import { log } from "./utils/cli.ts";
|
||||||
import { reportErrorToComment } from "./utils/errorReport.ts";
|
import { reportErrorToComment } from "./utils/errorReport.ts";
|
||||||
import {
|
import { createOctokit, parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
|
||||||
createOctokit,
|
|
||||||
parseRepoContext,
|
|
||||||
setupGitHubInstallationToken,
|
|
||||||
} from "./utils/github.ts";
|
|
||||||
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
||||||
import { Timer } from "./utils/timer.ts";
|
import { Timer } from "./utils/timer.ts";
|
||||||
|
|
||||||
// runtime validation using agents (needed for ArkType)
|
|
||||||
// Note: The AgentName type is defined in external.ts, this is the runtime validator
|
|
||||||
|
|
||||||
export const AgentInputKey = type.enumerated(
|
|
||||||
...Object.values(agents).flatMap((agent) => agent.apiKeyNames)
|
|
||||||
);
|
|
||||||
export type AgentInputKey = typeof AgentInputKey.infer;
|
|
||||||
|
|
||||||
const keyInputDefs = flatMorph(agents, (_, agent) =>
|
|
||||||
agent.apiKeyNames.map((inputKey) => [inputKey, "string | undefined?"] as const)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Inputs = type({
|
export const Inputs = type({
|
||||||
prompt: "string",
|
prompt: "string",
|
||||||
...keyInputDefs,
|
effort: Effort,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Inputs = typeof Inputs.infer;
|
export type Inputs = typeof Inputs.infer;
|
||||||
@@ -84,7 +67,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
|
|
||||||
// phase 3: resolve agent (needs repo settings)
|
// phase 3: resolve agent (needs repo settings)
|
||||||
const agent = resolveAgent({
|
const agent = resolveAgent({
|
||||||
inputs,
|
|
||||||
payload,
|
payload,
|
||||||
repoSettings: githubSetup.repoSettings,
|
repoSettings: githubSetup.repoSettings,
|
||||||
});
|
});
|
||||||
@@ -93,7 +75,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
// phase 4: validate API key (sync, needs agent) - fail fast before long-running operations
|
// phase 4: validate API key (sync, needs agent) - fail fast before long-running operations
|
||||||
const apiKeySetup = validateApiKey({
|
const apiKeySetup = validateApiKey({
|
||||||
agent,
|
agent,
|
||||||
inputs,
|
|
||||||
owner: githubSetup.owner,
|
owner: githubSetup.owner,
|
||||||
name: githubSetup.name,
|
name: githubSetup.name,
|
||||||
});
|
});
|
||||||
@@ -225,24 +206,19 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get agents that have matching API keys in the inputs
|
* Check if an agent has API keys available (from process.env)
|
||||||
*/
|
*/
|
||||||
/**
|
function agentHasApiKeys(agent: Agent): boolean {
|
||||||
* Check if an agent has API keys available (inputs or process.env for opencode)
|
|
||||||
*/
|
|
||||||
function agentHasApiKeys(agent: Agent, inputs: Inputs): boolean {
|
|
||||||
if (agent.name === "opencode") {
|
if (agent.name === "opencode") {
|
||||||
// check inputs first, then process.env
|
// opencode accepts any API_KEY from environment
|
||||||
const hasInputKey = Object.keys(inputs).some((key) => key.includes("api_key"));
|
|
||||||
if (hasInputKey) return true;
|
|
||||||
return Object.keys(process.env).some((key) => key.includes("API_KEY") && process.env[key]);
|
return Object.keys(process.env).some((key) => key.includes("API_KEY") && process.env[key]);
|
||||||
}
|
}
|
||||||
const inputsRecord = inputs as Record<string, string | undefined>;
|
// check if any of the agent's expected keys are in environment
|
||||||
return agent.apiKeyNames.some((inputKey) => inputsRecord[inputKey]);
|
return agent.apiKeyNames.some((envKey) => !!process.env[envKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableAgents(inputs: Inputs): Agent[] {
|
function getAvailableAgents(): Agent[] {
|
||||||
return Object.values(agents).filter((agent) => agentHasApiKeys(agent, inputs));
|
return Object.values(agents).filter((agent) => agentHasApiKeys(agent));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,7 +251,7 @@ function buildMissingApiKeyError(params: { agent: Agent; owner: string; name: st
|
|||||||
} else {
|
} else {
|
||||||
const inputKeys =
|
const inputKeys =
|
||||||
params.agent.apiKeyNames.length > 0 ? params.agent.apiKeyNames : getAllPossibleKeyNames();
|
params.agent.apiKeyNames.length > 0 ? params.agent.apiKeyNames : getAllPossibleKeyNames();
|
||||||
const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``);
|
const secretNames = inputKeys.map((key) => `\`${key}\``);
|
||||||
secretNameList = inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`;
|
secretNameList = inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,16 +337,16 @@ async function initializeGitHub(token: string): Promise<GitHubSetup> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveAgent({
|
function resolveAgent({
|
||||||
inputs,
|
|
||||||
payload,
|
payload,
|
||||||
repoSettings,
|
repoSettings,
|
||||||
}: {
|
}: {
|
||||||
inputs: Inputs;
|
|
||||||
payload: Payload;
|
payload: Payload;
|
||||||
repoSettings: RepoSettings;
|
repoSettings: RepoSettings;
|
||||||
}): Agent {
|
}): Agent {
|
||||||
const agentOverride = process.env.AGENT_OVERRIDE as AgentName | undefined;
|
const agentOverride = process.env.AGENT_OVERRIDE as AgentName | undefined;
|
||||||
log.debug(`» determineAgent: agentOverride=${agentOverride}, payload.agent=${payload.agent}, repoSettings.defaultAgent=${repoSettings.defaultAgent}`);
|
log.debug(
|
||||||
|
`» determineAgent: agentOverride=${agentOverride}, payload.agent=${payload.agent}, repoSettings.defaultAgent=${repoSettings.defaultAgent}`
|
||||||
|
);
|
||||||
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
|
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
|
||||||
|
|
||||||
if (configuredAgentName) {
|
if (configuredAgentName) {
|
||||||
@@ -388,13 +364,13 @@ function resolveAgent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for repo-level defaults, check if agent has matching keys before selecting
|
// for repo-level defaults, check if agent has matching keys before selecting
|
||||||
if (agentHasApiKeys(agent, inputs)) {
|
if (agentHasApiKeys(agent)) {
|
||||||
log.info(`Selected configured agent: ${agent.name}`);
|
log.info(`Selected configured agent: ${agent.name}`);
|
||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fall through to auto-selection
|
// fall through to auto-selection
|
||||||
const availableAgents = getAvailableAgents(inputs);
|
const availableAgents = getAvailableAgents();
|
||||||
log.warning(
|
log.warning(
|
||||||
`Repo default agent ${agent.name} has no matching API keys. Available: ${
|
`Repo default agent ${agent.name} has no matching API keys. Available: ${
|
||||||
availableAgents.map((a) => a.name).join(", ") || "none"
|
availableAgents.map((a) => a.name).join(", ") || "none"
|
||||||
@@ -402,7 +378,7 @@ function resolveAgent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableAgents = getAvailableAgents(inputs);
|
const availableAgents = getAvailableAgents();
|
||||||
if (availableAgents.length === 0) {
|
if (availableAgents.length === 0) {
|
||||||
throw new Error("no agents available - missing API keys");
|
throw new Error("no agents available - missing API keys");
|
||||||
}
|
}
|
||||||
@@ -425,8 +401,13 @@ function parsePayload(inputs: Inputs): Payload {
|
|||||||
if (!("~pullfrog" in parsedPrompt)) {
|
if (!("~pullfrog" in parsedPrompt)) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
return parsedPrompt as Payload;
|
// internal invocation: use effort from payload, fallback to input
|
||||||
|
return {
|
||||||
|
...parsedPrompt,
|
||||||
|
effort: parsedPrompt.effort ?? inputs.effort,
|
||||||
|
} as Payload;
|
||||||
} catch {
|
} catch {
|
||||||
|
// external invocation: use effort from input
|
||||||
return {
|
return {
|
||||||
"~pullfrog": true,
|
"~pullfrog": true,
|
||||||
agent: null,
|
agent: null,
|
||||||
@@ -435,6 +416,7 @@ function parsePayload(inputs: Inputs): Payload {
|
|||||||
trigger: "unknown",
|
trigger: "unknown",
|
||||||
},
|
},
|
||||||
modes,
|
modes,
|
||||||
|
effort: inputs.effort ?? "think",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,22 +429,22 @@ async function installAgentCli(params: { agent: Agent; token: string }): Promise
|
|||||||
return params.agent.install();
|
return params.agent.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectApiKeys(agent: Agent, inputs: Inputs): Record<string, string> {
|
function collectApiKeys(agent: Agent): Record<string, string> {
|
||||||
const apiKeys: Record<string, string> = {};
|
const apiKeys: Record<string, string> = {};
|
||||||
const inputsRecord = inputs as Record<string, string | undefined>;
|
|
||||||
|
|
||||||
for (const inputKey of agent.apiKeyNames) {
|
// read API keys from environment variables
|
||||||
const value = inputsRecord[inputKey];
|
for (const envKey of agent.apiKeyNames) {
|
||||||
|
const value = process.env[envKey];
|
||||||
if (value) {
|
if (value) {
|
||||||
apiKeys[inputKey] = value;
|
apiKeys[envKey] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for OpenCode: also check process.env for any API_KEY variables
|
// for OpenCode: check process.env for any API_KEY variables
|
||||||
if (agent.name === "opencode" && Object.keys(apiKeys).length === 0) {
|
if (agent.name === "opencode" && Object.keys(apiKeys).length === 0) {
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
if (value && typeof value === "string" && key.includes("API_KEY")) {
|
if (value && typeof value === "string" && key.includes("API_KEY")) {
|
||||||
apiKeys[key.toLowerCase()] = value;
|
apiKeys[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,13 +452,8 @@ function collectApiKeys(agent: Agent, inputs: Inputs): Record<string, string> {
|
|||||||
return apiKeys;
|
return apiKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApiKey(params: {
|
function validateApiKey(params: { agent: Agent; owner: string; name: string }): ApiKeySetup {
|
||||||
agent: Agent;
|
const apiKeys = collectApiKeys(params.agent);
|
||||||
inputs: Inputs;
|
|
||||||
owner: string;
|
|
||||||
name: string;
|
|
||||||
}): ApiKeySetup {
|
|
||||||
const apiKeys = collectApiKeys(params.agent, params.inputs);
|
|
||||||
|
|
||||||
if (Object.keys(apiKeys).length === 0) {
|
if (Object.keys(apiKeys).length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -497,7 +474,8 @@ function validateApiKey(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runAgent(ctx: AgentContext): Promise<AgentResult> {
|
async function runAgent(ctx: AgentContext): Promise<AgentResult> {
|
||||||
log.info(`Running ${ctx.agent.name}...`);
|
const effort = ctx.payload.effort ?? "think";
|
||||||
|
log.info(`Running ${ctx.agent.name} with effort=${effort}...`);
|
||||||
// strip context from event
|
// strip context from event
|
||||||
const { context: _context, ...eventWithoutContext } = ctx.payload.event;
|
const { context: _context, ...eventWithoutContext } = ctx.payload.event;
|
||||||
// format: prompt + two newlines + TOON encoded event
|
// format: prompt + two newlines + TOON encoded event
|
||||||
@@ -516,6 +494,7 @@ async function runAgent(ctx: AgentContext): Promise<AgentResult> {
|
|||||||
defaultBranch: ctx.repo.default_branch,
|
defaultBranch: ctx.repo.default_branch,
|
||||||
isPublic: !ctx.repo.private,
|
isPublic: !ctx.repo.private,
|
||||||
},
|
},
|
||||||
|
effort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user