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]
parent
9071c0ae6c
commit
ceadb3120a
@@ -139,6 +139,7 @@ export const 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) => {
|
||||
finalOutput += chunk;
|
||||
|
||||
|
||||
+6
-1
@@ -14,6 +14,10 @@ import { spawn } from "../utils/subprocess.ts";
|
||||
import { ThinkingTimer } from "../utils/timer.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
|
||||
// https://developers.openai.com/codex/models/
|
||||
type ModelReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
@@ -124,7 +128,7 @@ async function installCodex(): Promise<string> {
|
||||
|
||||
const cliPath = await installFromNpmTarball({
|
||||
packageName: "@openai/codex",
|
||||
version: "latest",
|
||||
version: CODEX_CLI_VERSION,
|
||||
executablePath: "bin/codex.js",
|
||||
installDependencies: true,
|
||||
});
|
||||
@@ -225,6 +229,7 @@ export const codex = agent({
|
||||
cwd: process.cwd(),
|
||||
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) => {
|
||||
finalOutput += chunk;
|
||||
|
||||
|
||||
+12
-4
@@ -10,10 +10,15 @@ import type { Effort } from "../external.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { markActivity } from "../utils/activity.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 { 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
|
||||
// only "max" overrides the model; mini/auto use default ("auto")
|
||||
const cursorEffortModels: Record<Effort, string | null> = {
|
||||
@@ -95,9 +100,12 @@ type CursorEvent =
|
||||
| CursorResultEvent;
|
||||
|
||||
async function installCursor(): Promise<string> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+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,
|
||||
// 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" },
|
||||
} as const;
|
||||
|
||||
@@ -85,6 +85,9 @@ type GeminiEvent =
|
||||
| GeminiToolResultEvent
|
||||
| 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.
|
||||
// these are server-side issues, not client errors.
|
||||
const TRANSIENT_ERROR_PATTERNS = [
|
||||
@@ -191,6 +194,7 @@ async function installGemini(githubInstallationToken?: string): Promise<string>
|
||||
return await installFromGithub({
|
||||
owner: "google-gemini",
|
||||
repo: "gemini-cli",
|
||||
tag: GEMINI_CLI_VERSION,
|
||||
assetName: "gemini.js",
|
||||
...(githubInstallationToken && { githubInstallationToken }),
|
||||
});
|
||||
@@ -231,6 +235,7 @@ export const gemini = agent({
|
||||
cmd: "node",
|
||||
args: [cliPath, ...args],
|
||||
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();
|
||||
finalOutput += text;
|
||||
|
||||
+6
-1
@@ -12,6 +12,10 @@ import { spawn } from "../utils/subprocess.ts";
|
||||
import { ThinkingTimer } from "../utils/timer.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).
|
||||
// when OpenCode encounters these, it often goes silent on stdout (Issue #752),
|
||||
// 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> {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "opencode-ai",
|
||||
version: "latest",
|
||||
version: OPENCODE_CLI_VERSION,
|
||||
executablePath: "bin/opencode",
|
||||
installDependencies: true,
|
||||
});
|
||||
@@ -114,6 +118,7 @@ export const opencode = agent({
|
||||
cwd: repoDir,
|
||||
env,
|
||||
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"],
|
||||
onStdout: async (chunk) => {
|
||||
const text = chunk.toString();
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+3
-3
@@ -311,12 +311,12 @@ async function runTestForAgent(ctx: RunContext): Promise<ValidationResult> {
|
||||
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") {
|
||||
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") {
|
||||
env.GEMINI_MODEL = "gemini-3-pro-preview";
|
||||
}
|
||||
|
||||
+70
-5
@@ -1,5 +1,5 @@
|
||||
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 { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -18,9 +18,16 @@ export interface InstallFromCurlParams {
|
||||
executableName: string;
|
||||
}
|
||||
|
||||
export interface InstallFromDirectTarballParams {
|
||||
url: string;
|
||||
executablePath: string;
|
||||
stripComponents?: number;
|
||||
}
|
||||
|
||||
export interface InstallFromGithubParams {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag?: string;
|
||||
assetName?: string;
|
||||
executablePath?: string;
|
||||
githubInstallationToken?: string;
|
||||
@@ -29,6 +36,7 @@ export interface InstallFromGithubParams {
|
||||
export interface InstallFromGithubTarballParams {
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag?: string;
|
||||
assetNamePattern: string;
|
||||
executablePath: string;
|
||||
githubInstallationToken?: string;
|
||||
@@ -181,8 +189,10 @@ async function fetchWithRetry(
|
||||
export async function installFromGithub(params: InstallFromGithubParams): Promise<string> {
|
||||
log.info(`» installing ${params.owner}/${params.repo} from GitHub releases...`);
|
||||
|
||||
// fetch release from GitHub API (latest)
|
||||
const releaseUrl = `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
|
||||
// fetch release from GitHub API (pinned tag or 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}...`);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
@@ -261,8 +271,10 @@ export async function installFromGithubTarball(
|
||||
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
||||
const assetName = params.assetNamePattern.replace("{os}", os).replace("{arch}", arch);
|
||||
|
||||
// fetch release from GitHub API (latest)
|
||||
const releaseUrl = `https://api.github.com/repos/${params.owner}/${params.repo}/releases/latest`;
|
||||
// fetch release from GitHub API (pinned tag or 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}...`);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
@@ -328,6 +340,59 @@ export async function installFromGithubTarball(
|
||||
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
|
||||
* 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