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:
committed by
pullfrog[bot]
co-authored by
Cursor
parent
9071c0ae6c
commit
ceadb3120a
@@ -139,6 +139,7 @@ export const claude = agent({
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
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) => {
|
onStdout: async (chunk) => {
|
||||||
finalOutput += chunk;
|
finalOutput += chunk;
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -14,6 +14,10 @@ import { spawn } from "../utils/subprocess.ts";
|
|||||||
import { ThinkingTimer } from "../utils/timer.ts";
|
import { ThinkingTimer } from "../utils/timer.ts";
|
||||||
import { type AgentRunContext, agent } from "./shared.ts";
|
import { type AgentRunContext, agent } from "./shared.ts";
|
||||||
|
|
||||||
|
// pinned CLI version — no 1-1 package.json dependency for the CLI package
|
||||||
|
// (package.json has @openai/codex-sdk which is the SDK, not the CLI)
|
||||||
|
const CODEX_CLI_VERSION = "0.101.0";
|
||||||
|
|
||||||
// configuration based on effort level
|
// configuration based on effort level
|
||||||
// https://developers.openai.com/codex/models/
|
// https://developers.openai.com/codex/models/
|
||||||
type ModelReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
type ModelReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
@@ -124,7 +128,7 @@ async function installCodex(): Promise<string> {
|
|||||||
|
|
||||||
const cliPath = await installFromNpmTarball({
|
const cliPath = await installFromNpmTarball({
|
||||||
packageName: "@openai/codex",
|
packageName: "@openai/codex",
|
||||||
version: "latest",
|
version: CODEX_CLI_VERSION,
|
||||||
executablePath: "bin/codex.js",
|
executablePath: "bin/codex.js",
|
||||||
installDependencies: true,
|
installDependencies: true,
|
||||||
});
|
});
|
||||||
@@ -225,6 +229,7 @@ export const codex = agent({
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env,
|
env,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
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) => {
|
onStdout: async (chunk) => {
|
||||||
finalOutput += chunk;
|
finalOutput += chunk;
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -10,10 +10,15 @@ import type { Effort } from "../external.ts";
|
|||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import { markActivity } from "../utils/activity.ts";
|
import { markActivity } from "../utils/activity.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { installFromCurl } from "../utils/install.ts";
|
import { installFromDirectTarball } from "../utils/install.ts";
|
||||||
import { ThinkingTimer } from "../utils/timer.ts";
|
import { ThinkingTimer } from "../utils/timer.ts";
|
||||||
import { type AgentRunContext, agent } from "./shared.ts";
|
import { type AgentRunContext, agent } from "./shared.ts";
|
||||||
|
|
||||||
|
// pinned CLI version — cursor-agent is downloaded as a tarball from downloads.cursor.com.
|
||||||
|
// the version format is {date}-{commit_hash}. update by inspecting the install script:
|
||||||
|
// curl -fsSL https://cursor.com/install | grep DOWNLOAD_URL
|
||||||
|
const CURSOR_CLI_VERSION = "2026.01.28-fd13201";
|
||||||
|
|
||||||
// effort configuration for Cursor
|
// effort configuration for Cursor
|
||||||
// only "max" overrides the model; mini/auto use default ("auto")
|
// only "max" overrides the model; mini/auto use default ("auto")
|
||||||
const cursorEffortModels: Record<Effort, string | null> = {
|
const cursorEffortModels: Record<Effort, string | null> = {
|
||||||
@@ -95,9 +100,12 @@ type CursorEvent =
|
|||||||
| CursorResultEvent;
|
| CursorResultEvent;
|
||||||
|
|
||||||
async function installCursor(): Promise<string> {
|
async function installCursor(): Promise<string> {
|
||||||
return await installFromCurl({
|
const os = process.platform === "darwin" ? "darwin" : "linux";
|
||||||
installUrl: "https://cursor.com/install",
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
||||||
executableName: "cursor-agent",
|
return await installFromDirectTarball({
|
||||||
|
url: `https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/${os}/${arch}/agent-cli-package.tar.gz`,
|
||||||
|
executablePath: "cursor-agent",
|
||||||
|
stripComponents: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -24,7 +24,7 @@ const geminiEffortConfig: Record<Effort, { model: string; thinkingLevel: string
|
|||||||
// pass the model directly it works if we ever did need to do something like this,
|
// pass the model directly it works if we ever did need to do something like this,
|
||||||
// we could write to .gemini/settings.json
|
// we could write to .gemini/settings.json
|
||||||
mini: { model: "gemini-3-flash-preview", thinkingLevel: "LOW" },
|
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" },
|
max: { model: "gemini-3-pro-preview", thinkingLevel: "HIGH" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -85,6 +85,9 @@ type GeminiEvent =
|
|||||||
| GeminiToolResultEvent
|
| GeminiToolResultEvent
|
||||||
| GeminiResultEvent;
|
| GeminiResultEvent;
|
||||||
|
|
||||||
|
// pinned CLI version — gemini-cli is installed from GitHub releases, not npm
|
||||||
|
const GEMINI_CLI_VERSION = "v0.28.2";
|
||||||
|
|
||||||
// transient API error patterns that warrant a retry.
|
// transient API error patterns that warrant a retry.
|
||||||
// these are server-side issues, not client errors.
|
// these are server-side issues, not client errors.
|
||||||
const TRANSIENT_ERROR_PATTERNS = [
|
const TRANSIENT_ERROR_PATTERNS = [
|
||||||
@@ -191,6 +194,7 @@ async function installGemini(githubInstallationToken?: string): Promise<string>
|
|||||||
return await installFromGithub({
|
return await installFromGithub({
|
||||||
owner: "google-gemini",
|
owner: "google-gemini",
|
||||||
repo: "gemini-cli",
|
repo: "gemini-cli",
|
||||||
|
tag: GEMINI_CLI_VERSION,
|
||||||
assetName: "gemini.js",
|
assetName: "gemini.js",
|
||||||
...(githubInstallationToken && { githubInstallationToken }),
|
...(githubInstallationToken && { githubInstallationToken }),
|
||||||
});
|
});
|
||||||
@@ -231,6 +235,7 @@ export const gemini = agent({
|
|||||||
cmd: "node",
|
cmd: "node",
|
||||||
args: [cliPath, ...args],
|
args: [cliPath, ...args],
|
||||||
env: process.env,
|
env: process.env,
|
||||||
|
activityTimeout: 0, // disabled: process-level timeout in main.ts handles this (subprocess timeout would kill orchestrator during delegation)
|
||||||
onStdout: async (chunk) => {
|
onStdout: async (chunk) => {
|
||||||
const text = chunk.toString();
|
const text = chunk.toString();
|
||||||
finalOutput += text;
|
finalOutput += text;
|
||||||
|
|||||||
+6
-1
@@ -12,6 +12,10 @@ import { spawn } from "../utils/subprocess.ts";
|
|||||||
import { ThinkingTimer } from "../utils/timer.ts";
|
import { ThinkingTimer } from "../utils/timer.ts";
|
||||||
import { type AgentRunContext, agent } from "./shared.ts";
|
import { type AgentRunContext, agent } from "./shared.ts";
|
||||||
|
|
||||||
|
// pinned CLI version — no 1-1 package.json dependency for the CLI package
|
||||||
|
// (package.json has @opencode-ai/sdk which is the SDK, not the CLI)
|
||||||
|
const OPENCODE_CLI_VERSION = "1.1.56";
|
||||||
|
|
||||||
// known provider error patterns in stderr (from --print-logs output).
|
// known provider error patterns in stderr (from --print-logs output).
|
||||||
// when OpenCode encounters these, it often goes silent on stdout (Issue #752),
|
// when OpenCode encounters these, it often goes silent on stdout (Issue #752),
|
||||||
// so we surface them prominently instead of burying them in debug warnings.
|
// so we surface them prominently instead of burying them in debug warnings.
|
||||||
@@ -37,7 +41,7 @@ function detectProviderError(text: string): string | null {
|
|||||||
async function installOpencode(): Promise<string> {
|
async function installOpencode(): Promise<string> {
|
||||||
return await installFromNpmTarball({
|
return await installFromNpmTarball({
|
||||||
packageName: "opencode-ai",
|
packageName: "opencode-ai",
|
||||||
version: "latest",
|
version: OPENCODE_CLI_VERSION,
|
||||||
executablePath: "bin/opencode",
|
executablePath: "bin/opencode",
|
||||||
installDependencies: true,
|
installDependencies: true,
|
||||||
});
|
});
|
||||||
@@ -114,6 +118,7 @@ export const opencode = agent({
|
|||||||
cwd: repoDir,
|
cwd: repoDir,
|
||||||
env,
|
env,
|
||||||
timeout: 600000, // 10 minutes timeout to prevent infinite hangs
|
timeout: 600000, // 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"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
onStdout: async (chunk) => {
|
onStdout: async (chunk) => {
|
||||||
const text = chunk.toString();
|
const text = chunk.toString();
|
||||||
|
|||||||
@@ -144427,7 +144427,7 @@ ${permalinkTip}`
|
|||||||
var modes = computeModes();
|
var modes = computeModes();
|
||||||
|
|
||||||
// agents/claude.ts
|
// 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";
|
import { join as join9 } from "node:path";
|
||||||
|
|
||||||
// package.json
|
// package.json
|
||||||
@@ -144519,7 +144519,7 @@ var package_default = {
|
|||||||
|
|
||||||
// utils/install.ts
|
// utils/install.ts
|
||||||
import { spawnSync as spawnSync4 } from "node:child_process";
|
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 { mkdtemp } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join as join8 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
@@ -144622,7 +144622,7 @@ async function fetchWithRetry(url4, headers, errorMessage) {
|
|||||||
}
|
}
|
||||||
async function installFromGithub(params) {
|
async function installFromGithub(params) {
|
||||||
log.info(`\xBB installing ${params.owner}/${params.repo} from GitHub releases...`);
|
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}...`);
|
log.debug(`\xBB fetching release from ${releaseUrl}...`);
|
||||||
const headers = {};
|
const headers = {};
|
||||||
if (params.githubInstallationToken) {
|
if (params.githubInstallationToken) {
|
||||||
@@ -144660,47 +144660,40 @@ async function installFromGithub(params) {
|
|||||||
log.info(`\xBB installed from GitHub release at ${cliPath}`);
|
log.info(`\xBB installed from GitHub release at ${cliPath}`);
|
||||||
return cliPath;
|
return cliPath;
|
||||||
}
|
}
|
||||||
async function installFromCurl(params) {
|
async function installFromDirectTarball(params) {
|
||||||
log.info(`\xBB installing ${params.executableName}...`);
|
log.info(`\xBB downloading tarball from ${params.url}...`);
|
||||||
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||||
const installScriptPath = join8(tempDir, "install.sh");
|
const tarballPath = join8(tempDir, "direct-package.tgz");
|
||||||
log.debug(`\xBB downloading install script from ${params.installUrl}...`);
|
const response = await fetch(params.url);
|
||||||
const installScriptResponse = await fetch(params.installUrl);
|
if (!response.ok) {
|
||||||
if (!installScriptResponse.ok) {
|
throw new Error(`failed to download tarball: ${response.status} ${response.statusText}`);
|
||||||
throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
|
|
||||||
}
|
}
|
||||||
if (!installScriptResponse.body) throw new Error("Response body is null");
|
if (!response.body) throw new Error("response body is null");
|
||||||
const fileStream = createWriteStream(installScriptPath);
|
const fileStream = createWriteStream(tarballPath);
|
||||||
await pipeline(installScriptResponse.body, fileStream);
|
await pipeline(response.body, fileStream);
|
||||||
log.debug(`\xBB downloaded install script to ${installScriptPath}`);
|
log.debug(`\xBB downloaded tarball to ${tarballPath}`);
|
||||||
chmodSync(installScriptPath, 493);
|
const extractDir = join8(tempDir, "direct-package");
|
||||||
log.debug(`\xBB installing to temp directory at ${tempDir}...`);
|
mkdirSync3(extractDir, { recursive: true });
|
||||||
const installResult = spawnSync4("bash", [installScriptPath], {
|
const tarArgs = ["-xzf", tarballPath, "-C", extractDir];
|
||||||
cwd: tempDir,
|
if (params.stripComponents) {
|
||||||
env: {
|
tarArgs.push(`--strip-components=${params.stripComponents}`);
|
||||||
// Run the install script with HOME set to temp directory
|
}
|
||||||
// ensuring a fresh install for each run
|
log.debug(`\xBB extracting tarball...`);
|
||||||
HOME: tempDir,
|
const extractResult = spawnSync4("tar", tarArgs, {
|
||||||
// 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
|
|
||||||
},
|
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
encoding: "utf-8"
|
encoding: "utf-8"
|
||||||
});
|
});
|
||||||
if (installResult.status !== 0) {
|
if (extractResult.status !== 0) {
|
||||||
const errorOutput = installResult.stderr || installResult.stdout || "No output";
|
|
||||||
throw new Error(
|
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)) {
|
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);
|
chmodSync(cliPath, 493);
|
||||||
log.info(`\xBB ${params.executableName} installed at ${cliPath}`);
|
log.info(`\xBB installed at ${cliPath}`);
|
||||||
return cliPath;
|
return cliPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144798,7 +144791,7 @@ function buildDisallowedTools(ctx) {
|
|||||||
}
|
}
|
||||||
function writeMcpConfig(ctx) {
|
function writeMcpConfig(ctx) {
|
||||||
const configDir = join9(ctx.tmpdir, ".claude");
|
const configDir = join9(ctx.tmpdir, ".claude");
|
||||||
mkdirSync3(configDir, { recursive: true });
|
mkdirSync4(configDir, { recursive: true });
|
||||||
const configPath = join9(configDir, "mcp.json");
|
const configPath = join9(configDir, "mcp.json");
|
||||||
const mcpConfig = {
|
const mcpConfig = {
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@@ -144861,6 +144854,8 @@ var claude = agent({
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
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) => {
|
onStdout: async (chunk) => {
|
||||||
finalOutput2 += chunk;
|
finalOutput2 += chunk;
|
||||||
stdoutBuffer += chunk;
|
stdoutBuffer += chunk;
|
||||||
@@ -144997,8 +144992,9 @@ var messageHandlers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// agents/codex.ts
|
// 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";
|
import { join as join10 } from "node:path";
|
||||||
|
var CODEX_CLI_VERSION = "0.101.0";
|
||||||
var PREFERRED_MODEL = "gpt-5.3-codex";
|
var PREFERRED_MODEL = "gpt-5.3-codex";
|
||||||
var FALLBACK_MODEL = "gpt-5.2-codex";
|
var FALLBACK_MODEL = "gpt-5.2-codex";
|
||||||
function getCodexEffortConfig(model) {
|
function getCodexEffortConfig(model) {
|
||||||
@@ -145038,7 +145034,7 @@ async function resolveModel(apiKey) {
|
|||||||
}
|
}
|
||||||
function writeCodexConfig(ctx) {
|
function writeCodexConfig(ctx) {
|
||||||
const codexDir = join10(ctx.tmpdir, ".codex");
|
const codexDir = join10(ctx.tmpdir, ".codex");
|
||||||
mkdirSync4(codexDir, { recursive: true });
|
mkdirSync5(codexDir, { recursive: true });
|
||||||
const configPath = join10(codexDir, "config.toml");
|
const configPath = join10(codexDir, "config.toml");
|
||||||
log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`);
|
log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`);
|
||||||
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]
|
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]
|
||||||
@@ -145077,7 +145073,7 @@ async function installCodex() {
|
|||||||
if (cachedCliPath) return cachedCliPath;
|
if (cachedCliPath) return cachedCliPath;
|
||||||
const cliPath = await installFromNpmTarball({
|
const cliPath = await installFromNpmTarball({
|
||||||
packageName: "@openai/codex",
|
packageName: "@openai/codex",
|
||||||
version: "latest",
|
version: CODEX_CLI_VERSION,
|
||||||
executablePath: "bin/codex.js",
|
executablePath: "bin/codex.js",
|
||||||
installDependencies: true
|
installDependencies: true
|
||||||
});
|
});
|
||||||
@@ -145140,6 +145136,8 @@ var codex = agent({
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: env2,
|
env: env2,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
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) => {
|
onStdout: async (chunk) => {
|
||||||
finalOutput2 += chunk;
|
finalOutput2 += chunk;
|
||||||
stdoutBuffer += chunk;
|
stdoutBuffer += chunk;
|
||||||
@@ -145275,10 +145273,11 @@ var messageHandlers2 = {
|
|||||||
|
|
||||||
// agents/cursor.ts
|
// agents/cursor.ts
|
||||||
import { spawn as spawn3 } from "node:child_process";
|
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 { homedir } from "node:os";
|
||||||
import { join as join11 } from "node:path";
|
import { join as join11 } from "node:path";
|
||||||
import { performance as performance6 } from "node:perf_hooks";
|
import { performance as performance6 } from "node:perf_hooks";
|
||||||
|
var CURSOR_CLI_VERSION = "2026.01.28-fd13201";
|
||||||
var cursorEffortModels = {
|
var cursorEffortModels = {
|
||||||
mini: null,
|
mini: null,
|
||||||
// use default (auto)
|
// use default (auto)
|
||||||
@@ -145287,9 +145286,12 @@ var cursorEffortModels = {
|
|||||||
max: "opus-4.5-thinking"
|
max: "opus-4.5-thinking"
|
||||||
};
|
};
|
||||||
async function installCursor() {
|
async function installCursor() {
|
||||||
return await installFromCurl({
|
const os = process.platform === "darwin" ? "darwin" : "linux";
|
||||||
installUrl: "https://cursor.com/install",
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
||||||
executableName: "cursor-agent"
|
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({
|
var cursor = agent({
|
||||||
@@ -145485,7 +145487,7 @@ function getCursorConfigDir() {
|
|||||||
function configureCursorMcpServers(ctx) {
|
function configureCursorMcpServers(ctx) {
|
||||||
const cursorConfigDir = getCursorConfigDir();
|
const cursorConfigDir = getCursorConfigDir();
|
||||||
const mcpConfigPath = join11(cursorConfigDir, "mcp.json");
|
const mcpConfigPath = join11(cursorConfigDir, "mcp.json");
|
||||||
mkdirSync5(cursorConfigDir, { recursive: true });
|
mkdirSync6(cursorConfigDir, { recursive: true });
|
||||||
const mcpServers = {
|
const mcpServers = {
|
||||||
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
|
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
|
||||||
};
|
};
|
||||||
@@ -145495,7 +145497,7 @@ function configureCursorMcpServers(ctx) {
|
|||||||
function configureCursorTools(ctx) {
|
function configureCursorTools(ctx) {
|
||||||
const cursorConfigDir = getCursorConfigDir();
|
const cursorConfigDir = getCursorConfigDir();
|
||||||
const cliConfigPath = join11(cursorConfigDir, "cli-config.json");
|
const cliConfigPath = join11(cursorConfigDir, "cli-config.json");
|
||||||
mkdirSync5(cursorConfigDir, { recursive: true });
|
mkdirSync6(cursorConfigDir, { recursive: true });
|
||||||
const bash = ctx.payload.bash;
|
const bash = ctx.payload.bash;
|
||||||
const deny = [];
|
const deny = [];
|
||||||
if (ctx.payload.search === "disabled") deny.push("WebSearch");
|
if (ctx.payload.search === "disabled") deny.push("WebSearch");
|
||||||
@@ -145518,7 +145520,7 @@ function configureCursorTools(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// agents/gemini.ts
|
// 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 { homedir as homedir2 } from "node:os";
|
||||||
import { join as join12 } from "node:path";
|
import { join as join12 } from "node:path";
|
||||||
var geminiEffortConfig = {
|
var geminiEffortConfig = {
|
||||||
@@ -145527,9 +145529,10 @@ var geminiEffortConfig = {
|
|||||||
// pass the model directly it works if we ever did need to do something like this,
|
// pass the model directly it works if we ever did need to do something like this,
|
||||||
// we could write to .gemini/settings.json
|
// we could write to .gemini/settings.json
|
||||||
mini: { model: "gemini-3-flash-preview", thinkingLevel: "LOW" },
|
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" }
|
max: { model: "gemini-3-pro-preview", thinkingLevel: "HIGH" }
|
||||||
};
|
};
|
||||||
|
var GEMINI_CLI_VERSION = "v0.28.2";
|
||||||
var TRANSIENT_ERROR_PATTERNS = [
|
var TRANSIENT_ERROR_PATTERNS = [
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"status: 500",
|
"status: 500",
|
||||||
@@ -145620,6 +145623,7 @@ async function installGemini(githubInstallationToken) {
|
|||||||
return await installFromGithub({
|
return await installFromGithub({
|
||||||
owner: "google-gemini",
|
owner: "google-gemini",
|
||||||
repo: "gemini-cli",
|
repo: "gemini-cli",
|
||||||
|
tag: GEMINI_CLI_VERSION,
|
||||||
assetName: "gemini.js",
|
assetName: "gemini.js",
|
||||||
...githubInstallationToken && { githubInstallationToken }
|
...githubInstallationToken && { githubInstallationToken }
|
||||||
});
|
});
|
||||||
@@ -145651,6 +145655,8 @@ var gemini = agent({
|
|||||||
cmd: "node",
|
cmd: "node",
|
||||||
args: [cliPath, ...args2],
|
args: [cliPath, ...args2],
|
||||||
env: process.env,
|
env: process.env,
|
||||||
|
activityTimeout: 0,
|
||||||
|
// disabled: process-level timeout in main.ts handles this (subprocess timeout would kill orchestrator during delegation)
|
||||||
onStdout: async (chunk) => {
|
onStdout: async (chunk) => {
|
||||||
const text = chunk.toString();
|
const text = chunk.toString();
|
||||||
finalOutput2 += text;
|
finalOutput2 += text;
|
||||||
@@ -145732,7 +145738,7 @@ function configureGeminiSettings(ctx) {
|
|||||||
const realHome = homedir2();
|
const realHome = homedir2();
|
||||||
const geminiConfigDir = join12(realHome, ".gemini");
|
const geminiConfigDir = join12(realHome, ".gemini");
|
||||||
const settingsPath = join12(geminiConfigDir, "settings.json");
|
const settingsPath = join12(geminiConfigDir, "settings.json");
|
||||||
mkdirSync6(geminiConfigDir, { recursive: true });
|
mkdirSync7(geminiConfigDir, { recursive: true });
|
||||||
let existingSettings = {};
|
let existingSettings = {};
|
||||||
try {
|
try {
|
||||||
const content = readFileSync6(settingsPath, "utf-8");
|
const content = readFileSync6(settingsPath, "utf-8");
|
||||||
@@ -145777,9 +145783,10 @@ function configureGeminiSettings(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// agents/opencode.ts
|
// 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 { join as join13 } from "node:path";
|
||||||
import { performance as performance7 } from "node:perf_hooks";
|
import { performance as performance7 } from "node:perf_hooks";
|
||||||
|
var OPENCODE_CLI_VERSION = "1.1.56";
|
||||||
var PROVIDER_ERROR_PATTERNS = [
|
var PROVIDER_ERROR_PATTERNS = [
|
||||||
{ pattern: "429", label: "rate limited (429)" },
|
{ pattern: "429", label: "rate limited (429)" },
|
||||||
{ pattern: "RESOURCE_EXHAUSTED", label: "quota exhausted" },
|
{ pattern: "RESOURCE_EXHAUSTED", label: "quota exhausted" },
|
||||||
@@ -145800,7 +145807,7 @@ function detectProviderError(text) {
|
|||||||
async function installOpencode() {
|
async function installOpencode() {
|
||||||
return await installFromNpmTarball({
|
return await installFromNpmTarball({
|
||||||
packageName: "opencode-ai",
|
packageName: "opencode-ai",
|
||||||
version: "latest",
|
version: OPENCODE_CLI_VERSION,
|
||||||
executablePath: "bin/opencode",
|
executablePath: "bin/opencode",
|
||||||
installDependencies: true
|
installDependencies: true
|
||||||
});
|
});
|
||||||
@@ -145812,7 +145819,7 @@ var opencode = agent({
|
|||||||
const cliPath = await installOpencode();
|
const cliPath = await installOpencode();
|
||||||
const tempHome = ctx.tmpdir;
|
const tempHome = ctx.tmpdir;
|
||||||
const configDir = join13(tempHome, ".config", "opencode");
|
const configDir = join13(tempHome, ".config", "opencode");
|
||||||
mkdirSync7(configDir, { recursive: true });
|
mkdirSync8(configDir, { recursive: true });
|
||||||
configureOpenCode(ctx);
|
configureOpenCode(ctx);
|
||||||
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||||
const modelOverride = process.env.OPENCODE_MODEL;
|
const modelOverride = process.env.OPENCODE_MODEL;
|
||||||
@@ -145850,6 +145857,8 @@ var opencode = agent({
|
|||||||
env: env2,
|
env: env2,
|
||||||
timeout: 6e5,
|
timeout: 6e5,
|
||||||
// 10 minutes timeout to prevent infinite hangs
|
// 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"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
onStdout: async (chunk) => {
|
onStdout: async (chunk) => {
|
||||||
const text = chunk.toString();
|
const text = chunk.toString();
|
||||||
@@ -145975,7 +145984,7 @@ ${stderrContext}`
|
|||||||
});
|
});
|
||||||
function configureOpenCode(ctx) {
|
function configureOpenCode(ctx) {
|
||||||
const configDir = join13(ctx.tmpdir, ".config", "opencode");
|
const configDir = join13(ctx.tmpdir, ".config", "opencode");
|
||||||
mkdirSync7(configDir, { recursive: true });
|
mkdirSync8(configDir, { recursive: true });
|
||||||
const configPath = join13(configDir, "opencode.json");
|
const configPath = join13(configDir, "opencode.json");
|
||||||
const opencodeMcpServers = {
|
const opencodeMcpServers = {
|
||||||
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
||||||
|
|||||||
+3
-3
@@ -311,12 +311,12 @@ async function runTestForAgent(ctx: RunContext): Promise<ValidationResult> {
|
|||||||
env.PULLFROG_TEST_REPO_SETUP = testConfig.repoSetup;
|
env.PULLFROG_TEST_REPO_SETUP = testConfig.repoSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
// opencode: set model override so tests use a model with quota (avoids default picking one without quota)
|
// opencode: override to google/gemini-3-pro-preview to avoid flash's tight RPD quota limits
|
||||||
if (ctx.agent === "opencode") {
|
if (ctx.agent === "opencode") {
|
||||||
env.OPENCODE_MODEL = "google/gemini-3-flash-preview";
|
env.OPENCODE_MODEL = "google/gemini-3-pro-preview";
|
||||||
}
|
}
|
||||||
|
|
||||||
// gemini: use pro model for tests to avoid flash's tight RPD quota limits
|
// gemini: override to pro for all tests (including mini-effort) to avoid flash's tight RPD quota limits
|
||||||
if (ctx.agent === "gemini") {
|
if (ctx.agent === "gemini") {
|
||||||
env.GEMINI_MODEL = "gemini-3-pro-preview";
|
env.GEMINI_MODEL = "gemini-3-pro-preview";
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-5
@@ -1,5 +1,5 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { chmodSync, createWriteStream, existsSync } from "node:fs";
|
import { chmodSync, createWriteStream, existsSync, mkdirSync } from "node:fs";
|
||||||
import { mkdtemp } from "node:fs/promises";
|
import { mkdtemp } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -18,9 +18,16 @@ export interface InstallFromCurlParams {
|
|||||||
executableName: string;
|
executableName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InstallFromDirectTarballParams {
|
||||||
|
url: string;
|
||||||
|
executablePath: string;
|
||||||
|
stripComponents?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InstallFromGithubParams {
|
export interface InstallFromGithubParams {
|
||||||
owner: string;
|
owner: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
|
tag?: string;
|
||||||
assetName?: string;
|
assetName?: string;
|
||||||
executablePath?: string;
|
executablePath?: string;
|
||||||
githubInstallationToken?: string;
|
githubInstallationToken?: string;
|
||||||
@@ -29,6 +36,7 @@ export interface InstallFromGithubParams {
|
|||||||
export interface InstallFromGithubTarballParams {
|
export interface InstallFromGithubTarballParams {
|
||||||
owner: string;
|
owner: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
|
tag?: string;
|
||||||
assetNamePattern: string;
|
assetNamePattern: string;
|
||||||
executablePath: string;
|
executablePath: string;
|
||||||
githubInstallationToken?: string;
|
githubInstallationToken?: string;
|
||||||
@@ -181,8 +189,10 @@ async function fetchWithRetry(
|
|||||||
export async function installFromGithub(params: InstallFromGithubParams): Promise<string> {
|
export async function installFromGithub(params: InstallFromGithubParams): Promise<string> {
|
||||||
log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
|
log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
|
||||||
|
|
||||||
// fetch release from GitHub API (latest)
|
// fetch release from GitHub API (pinned tag or latest)
|
||||||
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(`» fetching release from ${releaseUrl}...`);
|
log.debug(`» fetching release from ${releaseUrl}...`);
|
||||||
|
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
@@ -261,8 +271,10 @@ export async function installFromGithubTarball(
|
|||||||
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
||||||
const assetName = params.assetNamePattern.replace("{os}", os).replace("{arch}", arch);
|
const assetName = params.assetNamePattern.replace("{os}", os).replace("{arch}", arch);
|
||||||
|
|
||||||
// fetch release from GitHub API (latest)
|
// fetch release from GitHub API (pinned tag or latest)
|
||||||
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.info(`» fetching release from ${releaseUrl}...`);
|
log.info(`» fetching release from ${releaseUrl}...`);
|
||||||
|
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
@@ -328,6 +340,59 @@ export async function installFromGithubTarball(
|
|||||||
return cliPath;
|
return cliPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a CLI tool from a direct tarball URL.
|
||||||
|
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable.
|
||||||
|
*/
|
||||||
|
export async function installFromDirectTarball(
|
||||||
|
params: InstallFromDirectTarballParams
|
||||||
|
): Promise<string> {
|
||||||
|
log.info(`» downloading tarball from ${params.url}...`);
|
||||||
|
|
||||||
|
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
const tarballPath = join(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 (!response.body) throw new Error("response body is null");
|
||||||
|
|
||||||
|
const fileStream = createWriteStream(tarballPath);
|
||||||
|
await pipeline(response.body, fileStream);
|
||||||
|
log.debug(`» downloaded tarball to ${tarballPath}`);
|
||||||
|
|
||||||
|
// always extract into a dedicated directory
|
||||||
|
const extractDir = join(tempDir, "direct-package");
|
||||||
|
mkdirSync(extractDir, { recursive: true });
|
||||||
|
|
||||||
|
const tarArgs = ["-xzf", tarballPath, "-C", extractDir];
|
||||||
|
if (params.stripComponents) {
|
||||||
|
tarArgs.push(`--strip-components=${params.stripComponents}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`» extracting tarball...`);
|
||||||
|
const extractResult = spawnSync("tar", tarArgs, {
|
||||||
|
stdio: "pipe",
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
if (extractResult.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cliPath = join(extractDir, params.executablePath);
|
||||||
|
if (!existsSync(cliPath)) {
|
||||||
|
throw new Error(`executable not found in extracted tarball at ${cliPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
chmodSync(cliPath, 0o755);
|
||||||
|
log.info(`» installed at ${cliPath}`);
|
||||||
|
|
||||||
|
return cliPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a CLI tool from a curl-based install script
|
* Install a CLI tool from a curl-based install script
|
||||||
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
||||||
|
|||||||
Reference in New Issue
Block a user