diff --git a/agents/claude.ts b/agents/claude.ts index 444a081..7aff563 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -6,7 +6,6 @@ import { agent, installFromNpmTarball } from "./shared.ts"; export const claude = agent({ name: "claude", - inputKeys: ["anthropic_api_key"], install: async () => { const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest"; return await installFromNpmTarball({ diff --git a/agents/codex.ts b/agents/codex.ts index 72b96ff..d40f1c9 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -6,7 +6,6 @@ import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./ export const codex = agent({ name: "codex", - inputKeys: ["openai_api_key"], install: async () => { return await installFromNpmTarball({ packageName: "@openai/codex", diff --git a/agents/cursor.ts b/agents/cursor.ts index 2ac7b6f..99cc790 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -7,7 +7,6 @@ import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared export const cursor = agent({ name: "cursor", - inputKeys: ["cursor_api_key"], install: async () => { return await installFromCurl({ installUrl: "https://cursor.com/install", diff --git a/agents/gemini.ts b/agents/gemini.ts index 05ae52f..49d8f3e 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -6,7 +6,6 @@ import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./ export const gemini = agent({ name: "gemini", - inputKeys: ["google_api_key", "gemini_api_key"], install: async () => { return await installFromNpmTarball({ packageName: "@google/gemini-cli", diff --git a/agents/index.ts b/agents/index.ts index e491d56..d5e08fa 100644 --- a/agents/index.ts +++ b/agents/index.ts @@ -11,5 +11,3 @@ export const agents = { cursor, gemini, } satisfies Record; - -export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number]; diff --git a/agents/shared.ts b/agents/shared.ts index 8332b46..a60e757 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -3,7 +3,8 @@ import { chmodSync, createWriteStream, existsSync } from "node:fs"; import { join } from "node:path"; import { pipeline } from "node:stream/promises"; import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; -import type { Payload } from "../external.ts"; +import type { show } from "@ark/util"; +import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts"; import { log } from "../utils/cli.ts"; /** @@ -232,13 +233,18 @@ export async function installFromCurl({ return cliPath; } -export const agent = (agent: agent): agent => { - return agent; +export const agent = (input: input): defineAgent => { + return { ...input, ...agentsManifest[input.name] } as never; }; -export type Agent = { - name: string; - inputKeys: string[]; +export interface AgentInput { + name: AgentName; install: () => Promise; run: (config: AgentConfig) => Promise; -}; +} + +export interface Agent extends AgentInput, AgentManifest {} + +type agentManifest = (typeof agentsManifest)[name]; + +type defineAgent = show>; diff --git a/entry.ts b/entry.ts index 01e6595..dd2ac43 100644 --- a/entry.ts +++ b/entry.ts @@ -24,7 +24,7 @@ async function run(): Promise { prompt: core.getInput("prompt", { required: true }), agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined, ...flatMorph(agents, (_, agent) => - agent.inputKeys.map((inputKey) => [inputKey, core.getInput(inputKey)]) + agent.apiKeyNames.map((inputKey) => [inputKey, core.getInput(inputKey)]) ), }; diff --git a/external.ts b/external.ts index 9da7d53..2a00c87 100644 --- a/external.ts +++ b/external.ts @@ -7,31 +7,38 @@ import type { Mode } from "./modes.ts"; // mcp name constant -export const ghPullfrogMcpName = "gh-pullfrog"; +export const ghPullfrogMcpName = "gh_pullfrog"; + +export interface AgentManifest { + displayName: string; + apiKeyNames: string[]; +} // agent manifest - static metadata about available agents export const agentsManifest = { claude: { - name: "Claude Code", - apiKeys: ["anthropic_api_key"], + displayName: "Claude Code", + apiKeyNames: ["anthropic_api_key"], }, codex: { - name: "Codex CLI", - apiKeys: ["openai_api_key"], + displayName: "Codex CLI", + apiKeyNames: ["openai_api_key"], }, cursor: { - name: "Cursor CLI", - apiKeys: ["cursor_api_key"], + displayName: "Cursor CLI", + apiKeyNames: ["cursor_api_key"], }, gemini: { - name: "Gemini CLI", - apiKeys: ["google_api_key", "gemini_api_key"], + displayName: "Gemini CLI", + apiKeyNames: ["google_api_key", "gemini_api_key"], }, -} as const; +} as const satisfies Record; // agent name type - union of agent slugs export type AgentName = keyof typeof agentsManifest; +export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number]; + // payload type for agent execution export type Payload = { "~pullfrog": true; diff --git a/main.ts b/main.ts index a3189b1..608c765 100644 --- a/main.ts +++ b/main.ts @@ -24,12 +24,12 @@ import { setupGitAuth, setupGitConfig } from "./utils/setup.ts"; export const AgentName = type.enumerated(...Object.values(agents).map((agent) => agent.name)); export const AgentInputKey = type.enumerated( - ...Object.values(agents).flatMap((agent) => agent.inputKeys) + ...Object.values(agents).flatMap((agent) => agent.apiKeyNames) ); export type AgentInputKey = typeof AgentInputKey.infer; const keyInputDefs = flatMorph(agents, (_, agent) => - agent.inputKeys.map((inputKey) => [inputKey, "string | undefined?"] as const) + agent.apiKeyNames.map((inputKey) => [inputKey, "string | undefined?"] as const) ); export const Inputs = type({ @@ -102,7 +102,7 @@ function throwMissingApiKeyError({ // Find which agents have inputKeys that match the provided inputs const availableAgents = Object.values(agents).filter((agent) => - agent.inputKeys.some((inputKey) => inputs[inputKey]) + agent.apiKeyNames.some((inputKey) => inputs[inputKey]) ); let message = `Pullfrog is configured to use ${agentName}, but the associated API key was not provided. @@ -230,13 +230,13 @@ async function installAgentCli(ctx: MainContext): Promise { } function validateApiKey(ctx: MainContext): void { - const matchingInputKey = ctx.agent.inputKeys.find( + const matchingInputKey = ctx.agent.apiKeyNames.find( (inputKey: string) => ctx.inputs[inputKey as AgentInputKey] ); if (!matchingInputKey) { throwMissingApiKeyError({ agentName: ctx.agentName, - inputKeys: ctx.agent.inputKeys, + inputKeys: ctx.agent.apiKeyNames, repoContext: ctx.repoContext, inputs: ctx.inputs, }); diff --git a/todo.md b/todo.md index 4d80171..6159893 100644 --- a/todo.md +++ b/todo.md @@ -7,6 +7,8 @@ [] test if home directory mcp.json works if mcp.json is specified in repo [] add footer to the working comment ("executed by {agent}", link to pullfrog (homepage) w/ small logo?, feedback (create github issue), link to workflow run)- see https://github.com/colinhacks/zod/issues/5459#issuecomment-3548382991 [] avoid passing all of process.env into agents: minimum # of vars +[] external.ts align to agents +[] toon encode in prompt ## MAYBE @@ -32,4 +34,5 @@ [x] standardize mcp server [x] entry.js [x] split up prompts, load dynamically based on mode -[x] log.txt to stdout \ No newline at end of file +[x] log.txt to stdout +[x] rename mcp to use underscore \ No newline at end of file