pin all CLI installations to explicit versions and use pro models by default (#283)

* pin all CLI installations to explicit versions and use pro models by default

- codex: pin @openai/codex to 0.101.0 (was "latest")
- opencode: pin opencode-ai to 1.1.56 (was "latest")
- gemini: pin gemini-cli to v0.28.2 via new tag param on installFromGithub
- cursor: pin to 2026.01.28-fd13201 via direct tarball download (replaces curl install script)
- add installFromDirectTarball to install.ts for versioned tarball URLs
- gemini auto effort now uses pro-preview instead of flash-preview
- test runner model overrides updated to use pro-preview consistently

Co-authored-by: Cursor <cursoragent@cursor.com>

* increase activity timeout

* fix delegation timeout

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
David Blass
2026-02-12 22:48:29 +00:00
committed by pullfrog[bot]
parent 9071c0ae6c
commit ceadb3120a
8 changed files with 163 additions and 65 deletions
+59 -50
View File
@@ -144427,7 +144427,7 @@ ${permalinkTip}`
var modes = computeModes();
// agents/claude.ts
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync7 } from "node:fs";
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
import { join as join9 } from "node:path";
// package.json
@@ -144519,7 +144519,7 @@ var package_default = {
// utils/install.ts
import { spawnSync as spawnSync4 } from "node:child_process";
import { chmodSync, createWriteStream, existsSync as existsSync5 } from "node:fs";
import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join as join8 } from "node:path";
@@ -144622,7 +144622,7 @@ async function fetchWithRetry(url4, headers, errorMessage) {
}
async function installFromGithub(params) {
log.info(`\xBB installing ${params.owner}/${params.repo} from GitHub releases...`);
const releaseUrl = `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
const releaseUrl = params.tag ? `https://api.github.com/repos/${params.owner}/${params.repo}/releases/tags/${params.tag}` : `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
log.debug(`\xBB fetching release from ${releaseUrl}...`);
const headers = {};
if (params.githubInstallationToken) {
@@ -144660,47 +144660,40 @@ async function installFromGithub(params) {
log.info(`\xBB installed from GitHub release at ${cliPath}`);
return cliPath;
}
async function installFromCurl(params) {
log.info(`\xBB installing ${params.executableName}...`);
async function installFromDirectTarball(params) {
log.info(`\xBB downloading tarball from ${params.url}...`);
const tempDir = process.env.PULLFROG_TEMP_DIR;
const installScriptPath = join8(tempDir, "install.sh");
log.debug(`\xBB downloading install script from ${params.installUrl}...`);
const installScriptResponse = await fetch(params.installUrl);
if (!installScriptResponse.ok) {
throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
const tarballPath = join8(tempDir, "direct-package.tgz");
const response = await fetch(params.url);
if (!response.ok) {
throw new Error(`failed to download tarball: ${response.status} ${response.statusText}`);
}
if (!installScriptResponse.body) throw new Error("Response body is null");
const fileStream = createWriteStream(installScriptPath);
await pipeline(installScriptResponse.body, fileStream);
log.debug(`\xBB downloaded install script to ${installScriptPath}`);
chmodSync(installScriptPath, 493);
log.debug(`\xBB installing to temp directory at ${tempDir}...`);
const installResult = spawnSync4("bash", [installScriptPath], {
cwd: tempDir,
env: {
// Run the install script with HOME set to temp directory
// ensuring a fresh install for each run
HOME: tempDir,
// XDG_CONFIG_HOME must match HOME so CLI tools find config in the right place
XDG_CONFIG_HOME: join8(tempDir, ".config"),
SHELL: process.env.SHELL,
USER: process.env.USER
},
if (!response.body) throw new Error("response body is null");
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body, fileStream);
log.debug(`\xBB downloaded tarball to ${tarballPath}`);
const extractDir = join8(tempDir, "direct-package");
mkdirSync3(extractDir, { recursive: true });
const tarArgs = ["-xzf", tarballPath, "-C", extractDir];
if (params.stripComponents) {
tarArgs.push(`--strip-components=${params.stripComponents}`);
}
log.debug(`\xBB extracting tarball...`);
const extractResult = spawnSync4("tar", tarArgs, {
stdio: "pipe",
encoding: "utf-8"
});
if (installResult.status !== 0) {
const errorOutput = installResult.stderr || installResult.stdout || "No output";
if (extractResult.status !== 0) {
throw new Error(
`Failed to install ${params.executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
`failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "unknown error"}`
);
}
const cliPath = join8(tempDir, ".local", "bin", params.executableName);
const cliPath = join8(extractDir, params.executablePath);
if (!existsSync5(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`);
throw new Error(`executable not found in extracted tarball at ${cliPath}`);
}
chmodSync(cliPath, 493);
log.info(`\xBB ${params.executableName} installed at ${cliPath}`);
log.info(`\xBB installed at ${cliPath}`);
return cliPath;
}
@@ -144798,7 +144791,7 @@ function buildDisallowedTools(ctx) {
}
function writeMcpConfig(ctx) {
const configDir = join9(ctx.tmpdir, ".claude");
mkdirSync3(configDir, { recursive: true });
mkdirSync4(configDir, { recursive: true });
const configPath = join9(configDir, "mcp.json");
const mcpConfig = {
mcpServers: {
@@ -144861,6 +144854,8 @@ var claude = agent({
cwd: process.cwd(),
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
activityTimeout: 0,
// disabled: process-level timeout in main.ts handles this (subprocess timeout would kill orchestrator during delegation)
onStdout: async (chunk) => {
finalOutput2 += chunk;
stdoutBuffer += chunk;
@@ -144997,8 +144992,9 @@ var messageHandlers = {
};
// agents/codex.ts
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync8 } from "node:fs";
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
import { join as join10 } from "node:path";
var CODEX_CLI_VERSION = "0.101.0";
var PREFERRED_MODEL = "gpt-5.3-codex";
var FALLBACK_MODEL = "gpt-5.2-codex";
function getCodexEffortConfig(model) {
@@ -145038,7 +145034,7 @@ async function resolveModel(apiKey) {
}
function writeCodexConfig(ctx) {
const codexDir = join10(ctx.tmpdir, ".codex");
mkdirSync4(codexDir, { recursive: true });
mkdirSync5(codexDir, { recursive: true });
const configPath = join10(codexDir, "config.toml");
log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`);
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]
@@ -145077,7 +145073,7 @@ async function installCodex() {
if (cachedCliPath) return cachedCliPath;
const cliPath = await installFromNpmTarball({
packageName: "@openai/codex",
version: "latest",
version: CODEX_CLI_VERSION,
executablePath: "bin/codex.js",
installDependencies: true
});
@@ -145140,6 +145136,8 @@ var codex = agent({
cwd: process.cwd(),
env: env2,
stdio: ["ignore", "pipe", "pipe"],
activityTimeout: 0,
// disabled: process-level timeout in main.ts handles this (subprocess timeout would kill orchestrator during delegation)
onStdout: async (chunk) => {
finalOutput2 += chunk;
stdoutBuffer += chunk;
@@ -145275,10 +145273,11 @@ var messageHandlers2 = {
// agents/cursor.ts
import { spawn as spawn3 } from "node:child_process";
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync9 } from "node:fs";
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync9 } from "node:fs";
import { homedir } from "node:os";
import { join as join11 } from "node:path";
import { performance as performance6 } from "node:perf_hooks";
var CURSOR_CLI_VERSION = "2026.01.28-fd13201";
var cursorEffortModels = {
mini: null,
// use default (auto)
@@ -145287,9 +145286,12 @@ var cursorEffortModels = {
max: "opus-4.5-thinking"
};
async function installCursor() {
return await installFromCurl({
installUrl: "https://cursor.com/install",
executableName: "cursor-agent"
const os = process.platform === "darwin" ? "darwin" : "linux";
const arch = process.arch === "arm64" ? "arm64" : "x64";
return await installFromDirectTarball({
url: `https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/${os}/${arch}/agent-cli-package.tar.gz`,
executablePath: "cursor-agent",
stripComponents: 1
});
}
var cursor = agent({
@@ -145485,7 +145487,7 @@ function getCursorConfigDir() {
function configureCursorMcpServers(ctx) {
const cursorConfigDir = getCursorConfigDir();
const mcpConfigPath = join11(cursorConfigDir, "mcp.json");
mkdirSync5(cursorConfigDir, { recursive: true });
mkdirSync6(cursorConfigDir, { recursive: true });
const mcpServers = {
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
};
@@ -145495,7 +145497,7 @@ function configureCursorMcpServers(ctx) {
function configureCursorTools(ctx) {
const cursorConfigDir = getCursorConfigDir();
const cliConfigPath = join11(cursorConfigDir, "cli-config.json");
mkdirSync5(cursorConfigDir, { recursive: true });
mkdirSync6(cursorConfigDir, { recursive: true });
const bash = ctx.payload.bash;
const deny = [];
if (ctx.payload.search === "disabled") deny.push("WebSearch");
@@ -145518,7 +145520,7 @@ function configureCursorTools(ctx) {
}
// agents/gemini.ts
import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync10 } from "node:fs";
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync10 } from "node:fs";
import { homedir as homedir2 } from "node:os";
import { join as join12 } from "node:path";
var geminiEffortConfig = {
@@ -145527,9 +145529,10 @@ var geminiEffortConfig = {
// pass the model directly it works if we ever did need to do something like this,
// we could write to .gemini/settings.json
mini: { model: "gemini-3-flash-preview", thinkingLevel: "LOW" },
auto: { model: "gemini-3-flash-preview", thinkingLevel: "HIGH" },
auto: { model: "gemini-3-pro-preview", thinkingLevel: "HIGH" },
max: { model: "gemini-3-pro-preview", thinkingLevel: "HIGH" }
};
var GEMINI_CLI_VERSION = "v0.28.2";
var TRANSIENT_ERROR_PATTERNS = [
"INTERNAL",
"status: 500",
@@ -145620,6 +145623,7 @@ async function installGemini(githubInstallationToken) {
return await installFromGithub({
owner: "google-gemini",
repo: "gemini-cli",
tag: GEMINI_CLI_VERSION,
assetName: "gemini.js",
...githubInstallationToken && { githubInstallationToken }
});
@@ -145651,6 +145655,8 @@ var gemini = agent({
cmd: "node",
args: [cliPath, ...args2],
env: process.env,
activityTimeout: 0,
// disabled: process-level timeout in main.ts handles this (subprocess timeout would kill orchestrator during delegation)
onStdout: async (chunk) => {
const text = chunk.toString();
finalOutput2 += text;
@@ -145732,7 +145738,7 @@ function configureGeminiSettings(ctx) {
const realHome = homedir2();
const geminiConfigDir = join12(realHome, ".gemini");
const settingsPath = join12(geminiConfigDir, "settings.json");
mkdirSync6(geminiConfigDir, { recursive: true });
mkdirSync7(geminiConfigDir, { recursive: true });
let existingSettings = {};
try {
const content = readFileSync6(settingsPath, "utf-8");
@@ -145777,9 +145783,10 @@ function configureGeminiSettings(ctx) {
}
// agents/opencode.ts
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync11 } from "node:fs";
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
import { join as join13 } from "node:path";
import { performance as performance7 } from "node:perf_hooks";
var OPENCODE_CLI_VERSION = "1.1.56";
var PROVIDER_ERROR_PATTERNS = [
{ pattern: "429", label: "rate limited (429)" },
{ pattern: "RESOURCE_EXHAUSTED", label: "quota exhausted" },
@@ -145800,7 +145807,7 @@ function detectProviderError(text) {
async function installOpencode() {
return await installFromNpmTarball({
packageName: "opencode-ai",
version: "latest",
version: OPENCODE_CLI_VERSION,
executablePath: "bin/opencode",
installDependencies: true
});
@@ -145812,7 +145819,7 @@ var opencode = agent({
const cliPath = await installOpencode();
const tempHome = ctx.tmpdir;
const configDir = join13(tempHome, ".config", "opencode");
mkdirSync7(configDir, { recursive: true });
mkdirSync8(configDir, { recursive: true });
configureOpenCode(ctx);
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
const modelOverride = process.env.OPENCODE_MODEL;
@@ -145850,6 +145857,8 @@ var opencode = agent({
env: env2,
timeout: 6e5,
// 10 minutes timeout to prevent infinite hangs
activityTimeout: 0,
// disabled: process-level timeout in main.ts handles this (subprocess timeout would kill orchestrator during delegation)
stdio: ["ignore", "pipe", "pipe"],
onStdout: async (chunk) => {
const text = chunk.toString();
@@ -145975,7 +145984,7 @@ ${stderrContext}`
});
function configureOpenCode(ctx) {
const configDir = join13(ctx.tmpdir, ".config", "opencode");
mkdirSync7(configDir, { recursive: true });
mkdirSync8(configDir, { recursive: true });
const configPath = join13(configDir, "opencode.json");
const opencodeMcpServers = {
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }