add more codex logic
This commit is contained in:
+15
-41
@@ -1,31 +1,14 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { existsSync } from "node:fs";
|
|
||||||
|
|
||||||
import { log } from "../utils/cli.ts";
|
import { findCliPath, log } from "../utils/cli.ts";
|
||||||
import { type Agent, instructions } from "./shared.ts";
|
import { type Agent, instructions } from "./shared.ts";
|
||||||
|
|
||||||
let cachedCliPath: string | undefined;
|
|
||||||
|
|
||||||
export const codex: Agent = {
|
export const codex: Agent = {
|
||||||
install: async (): Promise<string> => {
|
install: async (): Promise<string> => {
|
||||||
if (cachedCliPath) {
|
const globalCodexPath = findCliPath("codex");
|
||||||
log.info(`Using cached Codex CLI at ${cachedCliPath}`);
|
if (globalCodexPath) {
|
||||||
return cachedCliPath;
|
log.info(`Using global Codex CLI at ${globalCodexPath}`);
|
||||||
}
|
return globalCodexPath;
|
||||||
|
|
||||||
// Check if codex CLI is already installed globally
|
|
||||||
try {
|
|
||||||
const result = spawnSync("which", ["codex"], { encoding: "utf-8" });
|
|
||||||
if (result.status === 0 && result.stdout) {
|
|
||||||
const globalCodexPath = result.stdout.trim();
|
|
||||||
if (globalCodexPath && existsSync(globalCodexPath)) {
|
|
||||||
cachedCliPath = globalCodexPath;
|
|
||||||
log.info(`Using global Codex CLI at ${globalCodexPath}`);
|
|
||||||
return cachedCliPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Codex CLI not found globally, continue with installation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install Codex CLI globally using npm
|
// Install Codex CLI globally using npm
|
||||||
@@ -41,14 +24,10 @@ export const codex: Agent = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify installation
|
// Verify installation
|
||||||
const verifyResult = spawnSync("which", ["codex"], { encoding: "utf-8" });
|
const installedPath = findCliPath("codex");
|
||||||
if (verifyResult.status === 0 && verifyResult.stdout) {
|
if (installedPath) {
|
||||||
const installedPath = verifyResult.stdout.trim();
|
log.info(`✓ Codex CLI installed at ${installedPath}`);
|
||||||
if (installedPath && existsSync(installedPath)) {
|
return installedPath;
|
||||||
cachedCliPath = installedPath;
|
|
||||||
log.info(`✓ Codex CLI installed at ${installedPath}`);
|
|
||||||
return cachedCliPath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Codex CLI installation completed but executable not found");
|
throw new Error("Codex CLI installation completed but executable not found");
|
||||||
@@ -58,13 +37,9 @@ export const codex: Agent = {
|
|||||||
throw new Error(`Codex CLI installation failed: ${errorMessage}`);
|
throw new Error(`Codex CLI installation failed: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
run: async ({ prompt, mcpServers, apiKey }) => {
|
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
|
||||||
process.env.OPENAI_API_KEY = apiKey;
|
process.env.OPENAI_API_KEY = apiKey;
|
||||||
|
|
||||||
if (!cachedCliPath) {
|
|
||||||
throw new Error("Codex CLI not installed. Call install() before run().");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure MCP servers for Codex (global config is fine - not part of repo)
|
// Configure MCP servers for Codex (global config is fine - not part of repo)
|
||||||
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
||||||
log.info("Configuring MCP servers for Codex...");
|
log.info("Configuring MCP servers for Codex...");
|
||||||
@@ -84,14 +59,14 @@ export const codex: Agent = {
|
|||||||
|
|
||||||
// Build the codex mcp add command with proper argument handling
|
// Build the codex mcp add command with proper argument handling
|
||||||
const addArgs = ["mcp", "add", serverName, "--", command, ...args];
|
const addArgs = ["mcp", "add", serverName, "--", command, ...args];
|
||||||
|
|
||||||
// Add environment variables as --env flags
|
// Add environment variables as --env flags
|
||||||
for (const [key, value] of Object.entries(envVars)) {
|
for (const [key, value] of Object.entries(envVars)) {
|
||||||
addArgs.push("--env", `${key}=${value}`);
|
addArgs.push("--env", `${key}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Adding MCP server '${serverName}'...`);
|
log.info(`Adding MCP server '${serverName}'...`);
|
||||||
const addResult = spawnSync("codex", addArgs, {
|
const addResult = spawnSync(cliPath, addArgs, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
env: {
|
env: {
|
||||||
@@ -99,7 +74,7 @@ export const codex: Agent = {
|
|||||||
OPENAI_API_KEY: apiKey,
|
OPENAI_API_KEY: apiKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addResult.status !== 0) {
|
if (addResult.status !== 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||||
@@ -117,9 +92,9 @@ export const codex: Agent = {
|
|||||||
|
|
||||||
// Use codex exec command via CLI
|
// Use codex exec command via CLI
|
||||||
const fullPrompt = `${instructions}\n\n****** USER PROMPT ******\n${prompt}`;
|
const fullPrompt = `${instructions}\n\n****** USER PROMPT ******\n${prompt}`;
|
||||||
|
|
||||||
log.info("Running Codex via CLI...");
|
log.info("Running Codex via CLI...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute codex via CLI using child_process
|
// Execute codex via CLI using child_process
|
||||||
const result = spawnSync("codex", ["exec", fullPrompt], {
|
const result = spawnSync("codex", ["exec", fullPrompt], {
|
||||||
@@ -159,4 +134,3 @@ export const codex: Agent = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { AgentName } from "../main.ts";
|
||||||
|
import { claude } from "./claude.ts";
|
||||||
|
import { codex } from "./codex.ts";
|
||||||
|
import type { Agent } from "./shared.ts";
|
||||||
|
|
||||||
|
export const agents = {
|
||||||
|
claude,
|
||||||
|
codex,
|
||||||
|
} as const satisfies Record<AgentName, Agent>;
|
||||||
@@ -20,11 +20,15 @@ export interface AgentConfig {
|
|||||||
githubInstallationToken: string;
|
githubInstallationToken: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
mcpServers: Record<string, McpServerConfig>;
|
mcpServers: Record<string, McpServerConfig>;
|
||||||
|
cliPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InputKey = "anthropic_api_key" | "openai_api_key";
|
||||||
|
|
||||||
export type Agent = {
|
export type Agent = {
|
||||||
install: () => Promise<string>;
|
install: () => Promise<string>;
|
||||||
run: (config: AgentConfig) => Promise<AgentResult>;
|
run: (config: AgentConfig) => Promise<AgentResult>;
|
||||||
|
inputKey: InputKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const instructions = `
|
export const instructions = `
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { claude } from "./agents/claude.ts";
|
|
||||||
import { codex } from "./agents/codex.ts";
|
|
||||||
import { createMcpConfigs } from "./mcp/config.ts";
|
import { createMcpConfigs } from "./mcp/config.ts";
|
||||||
import packageJson from "./package.json" with { type: "json" };
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
import { DEFAULT_REPO_SETTINGS, getRepoSettings, type RepoSettings } from "./utils/api.ts";
|
import { fetchRepoSettings } from "./utils/api.ts";
|
||||||
import { log } from "./utils/cli.ts";
|
import { log } from "./utils/cli.ts";
|
||||||
import {
|
import {
|
||||||
parseRepoContext,
|
parseRepoContext,
|
||||||
@@ -11,12 +9,16 @@ import {
|
|||||||
setupGitHubInstallationToken,
|
setupGitHubInstallationToken,
|
||||||
} from "./utils/github.ts";
|
} from "./utils/github.ts";
|
||||||
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
||||||
|
import { agents } from "./agents/index.ts";
|
||||||
|
|
||||||
|
export const AgentName = type.enumerated("codex", "claude");
|
||||||
|
export type AgentName = typeof AgentName.infer;
|
||||||
|
|
||||||
export const Inputs = type({
|
export const Inputs = type({
|
||||||
prompt: "string",
|
prompt: "string",
|
||||||
"anthropic_api_key?": "string | undefined",
|
"anthropic_api_key?": "string | undefined",
|
||||||
"openai_api_key?": "string | undefined",
|
"openai_api_key?": "string | undefined",
|
||||||
"agent?": "string | undefined",
|
"agent?": AgentName,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Inputs = typeof Inputs.infer;
|
export type Inputs = typeof Inputs.infer;
|
||||||
@@ -35,6 +37,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
try {
|
try {
|
||||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
|
|
||||||
|
Inputs.assert(inputs);
|
||||||
setupGitConfig();
|
setupGitConfig();
|
||||||
|
|
||||||
const { githubInstallationToken, wasAcquired, isFallbackToken } =
|
const { githubInstallationToken, wasAcquired, isFallbackToken } =
|
||||||
@@ -44,31 +47,15 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
}
|
}
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
// Fetch repo settings (agent, permissions, workflows) from API
|
const repoSettings = await fetchRepoSettings({
|
||||||
// Skip API call if we're using GITHUB_TOKEN fallback (app not installed)
|
token: githubInstallationToken,
|
||||||
let repoSettings: RepoSettings;
|
repoContext,
|
||||||
if (isFallbackToken) {
|
isFallbackToken,
|
||||||
log.info("Using default repository settings (app not installed)");
|
});
|
||||||
repoSettings = DEFAULT_REPO_SETTINGS;
|
|
||||||
} else {
|
|
||||||
log.info("Fetching repository settings...");
|
|
||||||
repoSettings = await getRepoSettings(githubInstallationToken, repoContext);
|
|
||||||
log.info("Repository settings fetched");
|
|
||||||
}
|
|
||||||
// Use agent from inputs if provided, otherwise use repo settings, default to claude
|
|
||||||
const agent = inputs.agent || repoSettings.defaultAgent || "claude";
|
|
||||||
|
|
||||||
// Agent registry
|
|
||||||
const agents = {
|
|
||||||
claude,
|
|
||||||
codex,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
if (!(agent in agents)) {
|
const agentName: AgentName = inputs.agent || repoSettings.defaultAgent || "claude";
|
||||||
throw new Error(`Unsupported agent: ${agent}. Supported agents: ${Object.keys(agents).join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentImpl = agents[agent as keyof typeof agents];
|
const agent = agents[agentName];
|
||||||
|
|
||||||
setupGitAuth(githubInstallationToken, repoContext);
|
setupGitAuth(githubInstallationToken, repoContext);
|
||||||
|
|
||||||
@@ -77,9 +64,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
||||||
|
|
||||||
// Install agent CLI before running
|
// Install agent CLI before running
|
||||||
await agentImpl.install();
|
const cliPath = await agent.install();
|
||||||
|
|
||||||
log.info(`Running ${agent} Agent SDK...`);
|
log.info(`Running ${agentName} Agent SDK...`);
|
||||||
log.box(inputs.prompt, { title: "Prompt" });
|
log.box(inputs.prompt, { title: "Prompt" });
|
||||||
|
|
||||||
// TODO: check if `inputs.prompts` is JSON
|
// TODO: check if `inputs.prompts` is JSON
|
||||||
@@ -88,25 +75,26 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
|
|
||||||
// Get API key based on agent type
|
// Get API key based on agent type
|
||||||
let apiKey: string;
|
let apiKey: string;
|
||||||
if (agent === "claude") {
|
if (agentName === "claude") {
|
||||||
if (!inputs.anthropic_api_key) {
|
if (!inputs.anthropic_api_key) {
|
||||||
throw new Error("ANTHROPIC_API_KEY is required for Claude agent");
|
throw new Error("ANTHROPIC_API_KEY is required for Claude agent");
|
||||||
}
|
}
|
||||||
apiKey = inputs.anthropic_api_key;
|
apiKey = inputs.anthropic_api_key;
|
||||||
} else if (agent === "codex") {
|
} else if (agentName === "codex") {
|
||||||
if (!inputs.openai_api_key) {
|
if (!inputs.openai_api_key) {
|
||||||
throw new Error("OPENAI_API_KEY is required for Codex agent");
|
throw new Error("OPENAI_API_KEY is required for Codex agent");
|
||||||
}
|
}
|
||||||
apiKey = inputs.openai_api_key;
|
apiKey = inputs.openai_api_key;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`API key configuration not implemented for agent: ${agent}`);
|
throw new Error(`API key configuration not implemented for agent: ${agentName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await agentImpl.run({
|
const result = await agent.run({
|
||||||
prompt: inputs.prompt,
|
prompt: inputs.prompt,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
githubInstallationToken,
|
githubInstallationToken,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
cliPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
+1
-1
@@ -17,6 +17,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "node esbuild.config.js",
|
"build": "node esbuild.config.js",
|
||||||
"play": "node play.ts",
|
"play": "node play.ts",
|
||||||
|
"scratch": "node scratch.ts",
|
||||||
"upDeps": "pnpm up --latest",
|
"upDeps": "pnpm up --latest",
|
||||||
"lock": "pnpm --ignore-workspace install",
|
"lock": "pnpm --ignore-workspace install",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
"@octokit/webhooks-types": "^7.6.1",
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"arktype": "2.1.25",
|
"arktype": "2.1.25",
|
||||||
"convex": "^1.29.0",
|
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"fastmcp": "^3.20.0",
|
"fastmcp": "^3.20.0",
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
|
function findCliPath(name: string): string | null {
|
||||||
|
|
||||||
|
const result = spawnSync("which", [name], { encoding: "utf-8" });
|
||||||
|
if (result.status === 0 && result.stdout) {
|
||||||
|
const cliPath = result.stdout.trim();
|
||||||
|
if (cliPath && existsSync(cliPath)) {
|
||||||
|
return cliPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(findCliPath("codei"));
|
||||||
+27
-1
@@ -1,7 +1,9 @@
|
|||||||
|
import type { AgentName } from "../main.ts";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
import type { RepoContext } from "./github.ts";
|
import type { RepoContext } from "./github.ts";
|
||||||
|
|
||||||
export interface RepoSettings {
|
export interface RepoSettings {
|
||||||
defaultAgent: string | null;
|
defaultAgent: AgentName | null;
|
||||||
webAccessLevel: "full_access" | "limited";
|
webAccessLevel: "full_access" | "limited";
|
||||||
webAccessAllowTrusted: boolean;
|
webAccessAllowTrusted: boolean;
|
||||||
webAccessDomains: string;
|
webAccessDomains: string;
|
||||||
@@ -21,6 +23,30 @@ export const DEFAULT_REPO_SETTINGS: RepoSettings = {
|
|||||||
workflows: [],
|
workflows: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch repository settings with fallback handling
|
||||||
|
* Uses API if token is available, otherwise returns defaults
|
||||||
|
*/
|
||||||
|
export async function fetchRepoSettings({
|
||||||
|
token,
|
||||||
|
repoContext,
|
||||||
|
isFallbackToken,
|
||||||
|
}: {
|
||||||
|
token: string;
|
||||||
|
repoContext: RepoContext;
|
||||||
|
isFallbackToken: boolean;
|
||||||
|
}): Promise<RepoSettings> {
|
||||||
|
if (isFallbackToken) {
|
||||||
|
log.info("Using default repository settings (app not installed)");
|
||||||
|
return DEFAULT_REPO_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Fetching repository settings...");
|
||||||
|
const settings = await getRepoSettings(token, repoContext);
|
||||||
|
log.info("Repository settings fetched");
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch repository settings from the Pullfrog API with fallback to defaults
|
* Fetch repository settings from the Pullfrog API with fallback to defaults
|
||||||
* Returns agent, permissions, and workflows (excludes triggers)
|
* Returns agent, permissions, and workflows (excludes triggers)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||||
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
||||||
@@ -262,3 +264,19 @@ export const log = {
|
|||||||
*/
|
*/
|
||||||
endGroup,
|
endGroup,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a CLI executable path by checking if it's installed globally
|
||||||
|
* @param name The name of the CLI executable to find
|
||||||
|
* @returns The path to the CLI executable, or null if not found
|
||||||
|
*/
|
||||||
|
export function findCliPath(name: string): string | null {
|
||||||
|
const result = spawnSync("which", [name], { encoding: "utf-8" });
|
||||||
|
if (result.status === 0 && result.stdout) {
|
||||||
|
const cliPath = result.stdout.trim();
|
||||||
|
if (cliPath && existsSync(cliPath)) {
|
||||||
|
return cliPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user