Code style (#97)
* Cleanup * fix: populate deny array before assigning to config, add CursorCliConfig type * Fix deny array ordering and add CursorCliConfig type Move deny array population before config declaration to avoid relying on reference semantics. Add proper type interface for the CLI config object. --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
0ccaa68d3a
commit
2d2d31adfa
+14
-14
@@ -3,7 +3,7 @@ import type { Effort } from "../external.ts";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, createAgentEnv, installFromNpmTarball, type ToolPermissions } from "./shared.ts";
|
||||
import { type AgentConfig, agent, createAgentEnv, installFromNpmTarball } from "./shared.ts";
|
||||
|
||||
// Model selection based on effort level
|
||||
// Note: mini uses Haiku for speed, auto uses opusplan for balance, max uses Opus for capability
|
||||
@@ -22,14 +22,14 @@ const claudeEffortModels: Record<Effort, string> = {
|
||||
/**
|
||||
* Build disallowedTools list from ToolPermissions.
|
||||
*/
|
||||
function buildDisallowedTools(tools: ToolPermissions): string[] {
|
||||
function buildDisallowedTools(ctx: AgentConfig): string[] {
|
||||
const disallowed: string[] = [];
|
||||
if (tools.web === "disabled") disallowed.push("WebFetch");
|
||||
if (tools.search === "disabled") disallowed.push("WebSearch");
|
||||
if (tools.write === "disabled") disallowed.push("Write");
|
||||
if (ctx.tools.web === "disabled") disallowed.push("WebFetch");
|
||||
if (ctx.tools.search === "disabled") disallowed.push("WebSearch");
|
||||
if (ctx.tools.write === "disabled") disallowed.push("Write");
|
||||
// both "disabled" and "restricted" block native bash
|
||||
// "restricted" means use MCP bash tool instead
|
||||
if (tools.bash !== "enabled") disallowed.push("Bash");
|
||||
if (ctx.tools.bash !== "enabled") disallowed.push("Bash");
|
||||
return disallowed;
|
||||
}
|
||||
|
||||
@@ -43,19 +43,19 @@ export const claude = agent({
|
||||
executablePath: "cli.js",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => {
|
||||
run: async (ctx) => {
|
||||
// Ensure API key is NOT in process.env - only pass via SDK's env option
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
const prompt = addInstructions({ payload, repo, tools });
|
||||
const prompt = addInstructions(ctx);
|
||||
log.group("Full prompt", () => log.info(prompt));
|
||||
|
||||
// select model based on effort level
|
||||
const model = claudeEffortModels[effort];
|
||||
log.info(`Using model: ${model} (effort: ${effort})`);
|
||||
const model = claudeEffortModels[ctx.effort];
|
||||
log.info(`Using model: ${model} (effort: ${ctx.effort})`);
|
||||
|
||||
// build disallowedTools based on tool permissions
|
||||
const disallowedTools = buildDisallowedTools(tools);
|
||||
const disallowedTools = buildDisallowedTools(ctx);
|
||||
if (disallowedTools.length > 0) {
|
||||
log.info(`🔒 disallowed tools: ${disallowedTools.join(", ")}`);
|
||||
}
|
||||
@@ -65,10 +65,10 @@ export const claude = agent({
|
||||
const queryOptions: Options = {
|
||||
permissionMode: "bypassPermissions" as const,
|
||||
disallowedTools,
|
||||
mcpServers,
|
||||
mcpServers: ctx.mcpServers,
|
||||
model,
|
||||
pathToClaudeCodeExecutable: cliPath,
|
||||
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }),
|
||||
pathToClaudeCodeExecutable: ctx.cliPath,
|
||||
env: createAgentEnv({ ANTHROPIC_API_KEY: ctx.apiKey }),
|
||||
};
|
||||
|
||||
const queryInstance = query({
|
||||
|
||||
+17
-27
@@ -1,6 +1,5 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
Codex,
|
||||
type CodexOptions,
|
||||
@@ -13,9 +12,9 @@ import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import {
|
||||
agent,
|
||||
type AgentConfig,
|
||||
installFromNpmTarball,
|
||||
setupProcessAgentEnv,
|
||||
type ToolPermissions,
|
||||
} from "./shared.ts";
|
||||
|
||||
// model configuration based on effort level
|
||||
@@ -35,20 +34,15 @@ const codexReasoningEffort: Record<Effort, ModelReasoningEffort | undefined> = {
|
||||
max: "high",
|
||||
};
|
||||
|
||||
interface WriteCodexConfigParams {
|
||||
tempHome: string;
|
||||
mcpServers: Record<string, McpHttpServerConfig>;
|
||||
tools: ToolPermissions;
|
||||
}
|
||||
|
||||
function writeCodexConfig({ tempHome, mcpServers, tools }: WriteCodexConfigParams): string {
|
||||
function writeCodexConfig(ctx: AgentConfig): string {
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const codexDir = join(tempHome, ".codex");
|
||||
mkdirSync(codexDir, { recursive: true });
|
||||
const configPath = join(codexDir, "config.toml");
|
||||
|
||||
// build MCP servers section
|
||||
const mcpServerSections: string[] = [];
|
||||
for (const [name, config] of Object.entries(mcpServers)) {
|
||||
for (const [name, config] of Object.entries(ctx.mcpServers)) {
|
||||
if (config.type !== "http") continue;
|
||||
log.info(`» adding MCP server '${name}' at ${config.url}`);
|
||||
mcpServerSections.push(`[mcp_servers.${name}]\nurl = "${config.url}"`);
|
||||
@@ -58,7 +52,7 @@ function writeCodexConfig({ tempHome, mcpServers, tools }: WriteCodexConfigParam
|
||||
// disable native shell if bash is "disabled" or "restricted"
|
||||
// when "restricted", agent uses MCP bash tool which filters secrets
|
||||
const features: string[] = [];
|
||||
if (tools.bash !== "enabled") {
|
||||
if (ctx.tools.bash !== "enabled") {
|
||||
features.push("shell_command_tool = false");
|
||||
features.push("unified_exec = false");
|
||||
}
|
||||
@@ -74,7 +68,7 @@ ${mcpServerSections.join("\n\n")}
|
||||
);
|
||||
|
||||
log.info(
|
||||
`» Codex config written to ${configPath} (shell: ${tools.bash === "enabled" ? "enabled" : "disabled"})`
|
||||
`» Codex config written to ${configPath} (shell: ${ctx.tools.bash === "enabled" ? "enabled" : "disabled"})`
|
||||
);
|
||||
|
||||
return codexDir;
|
||||
@@ -89,28 +83,24 @@ export const codex = agent({
|
||||
executablePath: "bin/codex.js",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => {
|
||||
run: async (ctx) => {
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
|
||||
// create config directory for codex before setting HOME
|
||||
const configDir = join(tempHome, ".config", "codex");
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
const codexDir = writeCodexConfig({
|
||||
tempHome,
|
||||
mcpServers,
|
||||
tools,
|
||||
});
|
||||
const codexDir = writeCodexConfig(ctx);
|
||||
|
||||
setupProcessAgentEnv({
|
||||
OPENAI_API_KEY: apiKey,
|
||||
OPENAI_API_KEY: ctx.apiKey,
|
||||
HOME: tempHome,
|
||||
CODEX_HOME: codexDir, // point Codex to our config directory
|
||||
});
|
||||
|
||||
// get model and reasoning effort based on effort level
|
||||
const model = codexModel[effort];
|
||||
const modelReasoningEffort = codexReasoningEffort[effort];
|
||||
const model = codexModel[ctx.effort];
|
||||
const modelReasoningEffort = codexReasoningEffort[ctx.effort];
|
||||
log.info(`Using model: ${model}`);
|
||||
if (modelReasoningEffort) {
|
||||
log.info(`Using modelReasoningEffort: ${modelReasoningEffort}`);
|
||||
@@ -118,8 +108,8 @@ export const codex = agent({
|
||||
|
||||
// Configure Codex
|
||||
const codexOptions: CodexOptions = {
|
||||
apiKey,
|
||||
codexPathOverride: cliPath,
|
||||
apiKey: ctx.apiKey,
|
||||
codexPathOverride: ctx.cliPath,
|
||||
};
|
||||
|
||||
const codex = new Codex(codexOptions);
|
||||
@@ -129,11 +119,11 @@ export const codex = agent({
|
||||
model,
|
||||
approvalPolicy: "never" as const,
|
||||
// write: "disabled" → read-only sandbox, otherwise full access for git ops
|
||||
sandboxMode: tools.write === "disabled" ? "read-only" : "danger-full-access",
|
||||
sandboxMode: ctx.tools.write === "disabled" ? "read-only" : "danger-full-access",
|
||||
// web: controls network access
|
||||
networkAccessEnabled: tools.web !== "disabled",
|
||||
networkAccessEnabled: ctx.tools.web !== "disabled",
|
||||
// search: controls web search
|
||||
webSearchEnabled: tools.search !== "disabled",
|
||||
webSearchEnabled: ctx.tools.search !== "disabled",
|
||||
...(modelReasoningEffort && { modelReasoningEffort }),
|
||||
};
|
||||
|
||||
@@ -144,7 +134,7 @@ export const codex = agent({
|
||||
const thread = codex.startThread(threadOptions);
|
||||
|
||||
try {
|
||||
const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo, tools }));
|
||||
const streamedTurn = await thread.runStreamed(addInstructions(ctx));
|
||||
|
||||
let finalOutput = "";
|
||||
for await (const event of streamedTurn.events) {
|
||||
|
||||
+33
-37
@@ -5,13 +5,7 @@ import { join } from "node:path";
|
||||
import type { Effort } from "../external.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import {
|
||||
agent,
|
||||
type ConfigureMcpServersParams,
|
||||
createAgentEnv,
|
||||
installFromCurl,
|
||||
type ToolPermissions,
|
||||
} from "./shared.ts";
|
||||
import { type AgentConfig, agent, createAgentEnv, installFromCurl } from "./shared.ts";
|
||||
|
||||
// effort configuration for Cursor
|
||||
// only "max" overrides the model; mini/auto use default ("auto")
|
||||
@@ -101,9 +95,9 @@ export const cursor = agent({
|
||||
executableName: "cursor-agent",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort, tools }) => {
|
||||
configureCursorMcpServers({ mcpServers, cliPath });
|
||||
configureCursorTools({ tools });
|
||||
run: async (ctx) => {
|
||||
configureCursorMcpServers(ctx);
|
||||
configureCursorTools(ctx);
|
||||
|
||||
// determine model based on effort level
|
||||
// respect project's .cursor/cli.json if it specifies a model
|
||||
@@ -116,19 +110,19 @@ export const cursor = agent({
|
||||
if (projectConfig.model) {
|
||||
log.info(`Using model from project .cursor/cli.json: ${projectConfig.model}`);
|
||||
} else {
|
||||
modelOverride = cursorEffortModels[effort];
|
||||
modelOverride = cursorEffortModels[ctx.effort];
|
||||
}
|
||||
} catch {
|
||||
modelOverride = cursorEffortModels[effort];
|
||||
modelOverride = cursorEffortModels[ctx.effort];
|
||||
}
|
||||
} else {
|
||||
modelOverride = cursorEffortModels[effort];
|
||||
modelOverride = cursorEffortModels[ctx.effort];
|
||||
}
|
||||
|
||||
if (modelOverride) {
|
||||
log.info(`Using model: ${modelOverride} (effort: ${effort})`);
|
||||
log.info(`Using model: ${modelOverride} (effort: ${ctx.effort})`);
|
||||
} else if (!existsSync(projectCliConfigPath)) {
|
||||
log.info(`Using default model (effort: ${effort})`);
|
||||
log.info(`Using default model (effort: ${ctx.effort})`);
|
||||
}
|
||||
|
||||
// track logged model_call_ids to avoid duplicates
|
||||
@@ -202,7 +196,7 @@ export const cursor = agent({
|
||||
};
|
||||
|
||||
try {
|
||||
const fullPrompt = addInstructions({ payload, repo, tools });
|
||||
const fullPrompt = addInstructions(ctx);
|
||||
log.group("Full prompt", () => log.info(fullPrompt));
|
||||
|
||||
// build CLI args
|
||||
@@ -221,10 +215,10 @@ export const cursor = agent({
|
||||
const startTime = Date.now();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(cliPath, cursorArgs, {
|
||||
const child = spawn(ctx.cliPath, cursorArgs, {
|
||||
cwd: process.cwd(),
|
||||
env: createAgentEnv({
|
||||
CURSOR_API_KEY: apiKey,
|
||||
CURSOR_API_KEY: ctx.apiKey,
|
||||
}),
|
||||
stdio: ["ignore", "pipe", "pipe"], // Ignore stdin, pipe stdout/stderr
|
||||
});
|
||||
@@ -317,7 +311,7 @@ export const cursor = agent({
|
||||
// There was an issue on macOS when you set HOME to a temp directory
|
||||
// it was unable to find the macOS keychain and would fail
|
||||
// temp solution is to stick with the actual $HOME
|
||||
function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
|
||||
function configureCursorMcpServers(ctx: AgentConfig): void {
|
||||
const realHome = homedir();
|
||||
const cursorConfigDir = join(realHome, ".cursor");
|
||||
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
||||
@@ -325,7 +319,7 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
|
||||
|
||||
// Convert to Cursor's expected format (HTTP config)
|
||||
const cursorMcpServers: Record<string, { type: string; url: string }> = {};
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) {
|
||||
if (serverConfig.type !== "http") {
|
||||
throw new Error(
|
||||
`Unsupported MCP server type for Cursor: ${(serverConfig as any).type || "unknown"}`
|
||||
@@ -342,8 +336,15 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
|
||||
log.info(`» MCP config written to ${mcpConfigPath}`);
|
||||
}
|
||||
|
||||
interface ConfigureCursorToolsParams {
|
||||
tools: ToolPermissions;
|
||||
interface CursorCliConfig {
|
||||
permissions: {
|
||||
allow: string[];
|
||||
deny: string[];
|
||||
};
|
||||
sandbox?: {
|
||||
mode: "enabled" | "disabled";
|
||||
networkAccess?: "allowlist" | "full";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,7 +353,7 @@ interface ConfigureCursorToolsParams {
|
||||
* Config path: $XDG_CONFIG_HOME/cursor/ (not ~/.cursor/) because createAgentEnv
|
||||
* sets XDG_CONFIG_HOME=$HOME/.config.
|
||||
*/
|
||||
function configureCursorTools({ tools }: ConfigureCursorToolsParams): void {
|
||||
function configureCursorTools(ctx: AgentConfig): void {
|
||||
const realHome = homedir();
|
||||
const cursorConfigDir = join(realHome, ".config", "cursor");
|
||||
const cliConfigPath = join(cursorConfigDir, "cli-config.json");
|
||||
@@ -360,23 +361,21 @@ function configureCursorTools({ tools }: ConfigureCursorToolsParams): void {
|
||||
|
||||
// build deny list based on tool permissions
|
||||
const deny: string[] = [];
|
||||
if (tools.search === "disabled") deny.push("WebSearch");
|
||||
if (tools.write === "disabled") deny.push("Write(**)");
|
||||
if (ctx.tools.search === "disabled") deny.push("WebSearch");
|
||||
if (ctx.tools.write === "disabled") deny.push("Write(**)");
|
||||
// both "disabled" and "restricted" block native shell
|
||||
if (tools.bash !== "enabled") deny.push("Shell(*)");
|
||||
if (ctx.tools.bash !== "enabled") deny.push("Shell(*)");
|
||||
|
||||
// web: "disabled" requires sandbox with network blocking
|
||||
// sandbox.networkAccess: "allowlist" blocks network in shell subprocesses via seatbelt
|
||||
const needsSandbox = tools.web === "disabled";
|
||||
|
||||
const config: Record<string, unknown> = {
|
||||
const config: CursorCliConfig = {
|
||||
permissions: {
|
||||
allow: tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"],
|
||||
allow: ctx.tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"],
|
||||
deny,
|
||||
},
|
||||
};
|
||||
|
||||
if (needsSandbox) {
|
||||
// web: "disabled" requires sandbox with network blocking
|
||||
// sandbox.networkAccess: "allowlist" blocks network in shell subprocesses via seatbelt
|
||||
if (ctx.tools.web === "disabled") {
|
||||
config.sandbox = {
|
||||
mode: "enabled",
|
||||
networkAccess: "allowlist",
|
||||
@@ -384,8 +383,5 @@ function configureCursorTools({ tools }: ConfigureCursorToolsParams): void {
|
||||
}
|
||||
|
||||
writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
log.info(`» CLI config written to ${cliConfigPath}`);
|
||||
log.info(
|
||||
`🔧 Cursor permissions: allow=${(config.permissions as { allow: string[] }).allow.join(",")}, deny=${deny.join(",") || "(none)"}, sandbox=${needsSandbox ? "enabled" : "disabled"}`
|
||||
);
|
||||
log.info(`» CLI config written to ${cliConfigPath}`, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
+19
-36
@@ -5,13 +5,7 @@ import type { Effort } from "../external.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { spawn } from "../utils/subprocess.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import {
|
||||
agent,
|
||||
type ConfigureMcpServersParams,
|
||||
createAgentEnv,
|
||||
installFromGithub,
|
||||
type ToolPermissions,
|
||||
} from "./shared.ts";
|
||||
import { type AgentConfig, agent, createAgentEnv, installFromGithub } from "./shared.ts";
|
||||
|
||||
// effort configuration: model + thinking level
|
||||
// thinkingLevel is set via settings.json modelConfig.generateContentConfig.thinkingConfig
|
||||
@@ -172,18 +166,14 @@ export const gemini = agent({
|
||||
...(githubInstallationToken && { githubInstallationToken }),
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort, tools }) => {
|
||||
// get model and thinking level based on effort
|
||||
const { model, thinkingLevel } = geminiEffortConfig[effort];
|
||||
log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`);
|
||||
run: async (ctx) => {
|
||||
const model = configureGeminiSettings(ctx);
|
||||
|
||||
configureGeminiSettings({ mcpServers, tools, thinkingLevel });
|
||||
|
||||
if (!apiKey) {
|
||||
if (!ctx.apiKey) {
|
||||
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
|
||||
}
|
||||
|
||||
const sessionPrompt = addInstructions({ payload, repo, tools });
|
||||
const sessionPrompt = addInstructions(ctx);
|
||||
log.group("Full prompt", () => log.info(sessionPrompt));
|
||||
|
||||
// build CLI args - --yolo for auto-approval
|
||||
@@ -196,8 +186,8 @@ export const gemini = agent({
|
||||
try {
|
||||
const result = await spawn({
|
||||
cmd: "node",
|
||||
args: [cliPath, ...args],
|
||||
env: createAgentEnv({ GEMINI_API_KEY: apiKey }),
|
||||
args: [ctx.cliPath, ...args],
|
||||
env: createAgentEnv({ GEMINI_API_KEY: ctx.apiKey }),
|
||||
onStdout: async (chunk) => {
|
||||
const text = chunk.toString();
|
||||
finalOutput += text;
|
||||
@@ -270,25 +260,16 @@ export const gemini = agent({
|
||||
},
|
||||
});
|
||||
|
||||
interface ConfigureGeminiParams {
|
||||
mcpServers: ConfigureMcpServersParams["mcpServers"];
|
||||
tools: ToolPermissions;
|
||||
thinkingLevel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Gemini CLI settings by writing to settings.json.
|
||||
* - MCP servers: uses `httpUrl` for HTTP/streamable transport
|
||||
* - thinkingLevel: configured via modelConfig.generateContentConfig.thinkingConfig
|
||||
* - tools.exclude: disables native tools based on ToolPermissions (v0.3.0+ format)
|
||||
* Returns the model to use for CLI args.
|
||||
*
|
||||
* See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md
|
||||
*/
|
||||
function configureGeminiSettings({
|
||||
mcpServers,
|
||||
tools,
|
||||
thinkingLevel,
|
||||
}: ConfigureGeminiParams): void {
|
||||
function configureGeminiSettings(ctx: AgentConfig): string {
|
||||
const { model, thinkingLevel } = geminiEffortConfig[ctx.effort];
|
||||
log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`);
|
||||
|
||||
const realHome = homedir();
|
||||
const geminiConfigDir = join(realHome, ".gemini");
|
||||
const settingsPath = join(geminiConfigDir, "settings.json");
|
||||
@@ -319,7 +300,7 @@ function configureGeminiSettings({
|
||||
excludeTools?: string[];
|
||||
}
|
||||
const geminiMcpServers: Record<string, GeminiMcpServerConfig> = {};
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) {
|
||||
if (serverConfig.type !== "http") {
|
||||
throw new Error(
|
||||
`Unsupported MCP server type for Gemini: ${(serverConfig as { type?: string }).type || "unknown"}`
|
||||
@@ -334,10 +315,10 @@ function configureGeminiSettings({
|
||||
|
||||
// build tools.exclude based on permissions (v0.3.0+ nested format)
|
||||
const exclude: string[] = [];
|
||||
if (tools.bash !== "enabled") exclude.push("run_shell_command");
|
||||
if (tools.write === "disabled") exclude.push("write_file");
|
||||
if (tools.web === "disabled") exclude.push("web_fetch");
|
||||
if (tools.search === "disabled") exclude.push("google_web_search");
|
||||
if (ctx.tools.bash !== "enabled") exclude.push("run_shell_command");
|
||||
if (ctx.tools.write === "disabled") exclude.push("write_file");
|
||||
if (ctx.tools.web === "disabled") exclude.push("web_fetch");
|
||||
if (ctx.tools.search === "disabled") exclude.push("google_web_search");
|
||||
|
||||
// merge with existing settings, overwriting mcpServers and modelConfig
|
||||
const newSettings: Record<string, unknown> = {
|
||||
@@ -361,4 +342,6 @@ function configureGeminiSettings({
|
||||
if (exclude.length > 0) {
|
||||
log.info(`🔒 excluded tools: ${exclude.join(", ")}`);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
+10
-17
@@ -3,14 +3,7 @@ import { encode as toonEncode } from "@toon-format/toon";
|
||||
import type { Payload } from "../external.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { getModes } from "../modes.ts";
|
||||
import type { ToolPermissions } from "./shared.ts";
|
||||
|
||||
interface RepoInfo {
|
||||
owner: string;
|
||||
name: string;
|
||||
defaultBranch: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
import type { RepoInfo, ToolPermissions } from "./shared.ts";
|
||||
|
||||
/**
|
||||
* Build runtime context string with git status, repo data, and GitHub Actions variables
|
||||
@@ -52,7 +45,7 @@ function buildRuntimeContext(repo: RepoInfo): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
interface AddInstructionsParams {
|
||||
interface AddInstructionsCtx {
|
||||
payload: Payload;
|
||||
repo: RepoInfo;
|
||||
tools: ToolPermissions;
|
||||
@@ -76,19 +69,19 @@ function getShellInstructions(bash: ToolPermissions["bash"]): string {
|
||||
}
|
||||
}
|
||||
|
||||
export const addInstructions = ({ payload, repo, tools }: AddInstructionsParams) => {
|
||||
export const addInstructions = (ctx: AddInstructionsCtx) => {
|
||||
let encodedEvent = "";
|
||||
|
||||
const eventKeys = Object.keys(payload.event);
|
||||
const eventKeys = Object.keys(ctx.payload.event);
|
||||
if (eventKeys.length === 1 && eventKeys[0] === "trigger") {
|
||||
// no meaningful event data to encode
|
||||
} else {
|
||||
// extract only essential fields to reduce token usage
|
||||
// const essentialEvent = payload.event;
|
||||
encodedEvent = toonEncode(payload.event);
|
||||
// const essentialEvent = ctx.payload.event;
|
||||
encodedEvent = toonEncode(ctx.payload.event);
|
||||
}
|
||||
|
||||
const runtimeContext = buildRuntimeContext(repo);
|
||||
const runtimeContext = buildRuntimeContext(ctx.repo);
|
||||
|
||||
return (
|
||||
`
|
||||
@@ -159,7 +152,7 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
|
||||
|
||||
**Efficiency**: Trust the tools - do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error.
|
||||
|
||||
${getShellInstructions(tools.bash)}
|
||||
${getShellInstructions(ctx.tools.bash)}
|
||||
|
||||
**Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished.
|
||||
|
||||
@@ -180,7 +173,7 @@ ${getShellInstructions(tools.bash)}
|
||||
|
||||
### Available modes
|
||||
|
||||
${[...getModes({ disableProgressComment: payload.disableProgressComment }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||
${[...getModes({ disableProgressComment: ctx.payload.disableProgressComment }), ...ctx.payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||
|
||||
### Following the mode instructions
|
||||
|
||||
@@ -190,7 +183,7 @@ Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\`
|
||||
|
||||
************* USER PROMPT *************
|
||||
|
||||
${payload.prompt
|
||||
${ctx.payload.prompt
|
||||
.split("\n")
|
||||
.map((line) => `> ${line}`)
|
||||
.join("\n")}
|
||||
|
||||
+12
-27
@@ -5,11 +5,10 @@ import { spawn } from "../utils/subprocess.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import {
|
||||
agent,
|
||||
type ConfigureMcpServersParams,
|
||||
type AgentConfig,
|
||||
createAgentEnv,
|
||||
installFromNpmTarball,
|
||||
setupProcessAgentEnv,
|
||||
type ToolPermissions,
|
||||
} from "./shared.ts";
|
||||
|
||||
export const opencode = agent({
|
||||
@@ -22,24 +21,15 @@ export const opencode = agent({
|
||||
installDependencies: true,
|
||||
});
|
||||
},
|
||||
run: async ({
|
||||
payload,
|
||||
apiKey: _apiKey,
|
||||
apiKeys,
|
||||
mcpServers,
|
||||
cliPath,
|
||||
repo,
|
||||
effort: _effort,
|
||||
tools,
|
||||
}) => {
|
||||
run: async (ctx) => {
|
||||
// 1. configure home/config directory
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const configDir = join(tempHome, ".config", "opencode");
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
configureOpenCode({ mcpServers, tools });
|
||||
configureOpenCode(ctx);
|
||||
|
||||
const prompt = addInstructions({ payload, repo, tools });
|
||||
const prompt = addInstructions(ctx);
|
||||
log.group("Full prompt", () => log.info(prompt));
|
||||
|
||||
// message positional must come right after "run", before flags
|
||||
@@ -60,7 +50,7 @@ export const opencode = agent({
|
||||
delete env.GITHUB_TOKEN;
|
||||
|
||||
// add API keys from apiKeys object
|
||||
for (const [key, value] of Object.entries(apiKeys || {})) {
|
||||
for (const [key, value] of Object.entries(ctx.apiKeys || {})) {
|
||||
env[key.toUpperCase()] = value;
|
||||
// also set GOOGLE_GENERATIVE_AI_API_KEY for Google provider compatibility
|
||||
if (key === "GEMINI_API_KEY") {
|
||||
@@ -71,7 +61,7 @@ export const opencode = agent({
|
||||
// run OpenCode in the repository directory (process.cwd() is set to GITHUB_WORKSPACE or repo dir)
|
||||
const repoDir = process.cwd();
|
||||
|
||||
log.info(`🚀 Starting OpenCode CLI: ${cliPath} ${args.join(" ")}`);
|
||||
log.info(`🚀 Starting OpenCode CLI: ${ctx.cliPath} ${args.join(" ")}`);
|
||||
log.info(`📁 Working directory: ${repoDir}`);
|
||||
log.debug(`🏠 HOME: ${env.HOME}`);
|
||||
log.debug(`📋 XDG_CONFIG_HOME: ${env.XDG_CONFIG_HOME}`);
|
||||
@@ -83,7 +73,7 @@ export const opencode = agent({
|
||||
let output = "";
|
||||
let stdoutBuffer = ""; // buffer for incomplete lines across chunks
|
||||
const result = await spawn({
|
||||
cmd: cliPath,
|
||||
cmd: ctx.cliPath,
|
||||
args,
|
||||
cwd: repoDir,
|
||||
env,
|
||||
@@ -191,16 +181,11 @@ export const opencode = agent({
|
||||
},
|
||||
});
|
||||
|
||||
interface ConfigureOpenCodeParams {
|
||||
mcpServers: ConfigureMcpServersParams["mcpServers"];
|
||||
tools: ToolPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure OpenCode via opencode.json config file.
|
||||
* Builds complete config with MCP servers and permissions in a single write to avoid race conditions.
|
||||
*/
|
||||
function configureOpenCode({ mcpServers, tools }: ConfigureOpenCodeParams): void {
|
||||
function configureOpenCode(ctx: AgentConfig): void {
|
||||
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||
const configDir = join(tempHome, ".config", "opencode");
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
@@ -208,7 +193,7 @@ function configureOpenCode({ mcpServers, tools }: ConfigureOpenCodeParams): void
|
||||
|
||||
// build MCP servers config
|
||||
const opencodeMcpServers: Record<string, { type: "remote"; url: string }> = {};
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) {
|
||||
if (serverConfig.type !== "http") {
|
||||
log.error(
|
||||
`unsupported MCP server type for OpenCode: ${(serverConfig as never as { type: string }).type || "unknown"}`
|
||||
@@ -227,9 +212,9 @@ function configureOpenCode({ mcpServers, tools }: ConfigureOpenCodeParams): void
|
||||
// build permission object based on tool permissions
|
||||
// note: OpenCode has no built-in web search tool
|
||||
const permission = {
|
||||
edit: tools.write === "disabled" ? "deny" : "allow",
|
||||
bash: tools.bash !== "enabled" ? "deny" : "allow",
|
||||
webfetch: tools.web === "disabled" ? "deny" : "allow",
|
||||
edit: ctx.tools.write === "disabled" ? "deny" : "allow",
|
||||
bash: ctx.tools.bash !== "enabled" ? "deny" : "allow",
|
||||
webfetch: ctx.tools.web === "disabled" ? "deny" : "allow",
|
||||
doom_loop: "allow",
|
||||
external_directory: "allow",
|
||||
};
|
||||
|
||||
@@ -66,14 +66,6 @@ export interface AgentConfig {
|
||||
tools: ToolPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for configuring MCP servers
|
||||
*/
|
||||
export interface ConfigureMcpServersParams {
|
||||
mcpServers: Record<string, McpHttpServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add agent-specific vars to a whitelisted environment object for agent subprocesses.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user