feat: add Claude Code agent for Anthropic model users (#502)
* feat: add Claude Code agent for Anthropic model users Re-adds Claude Code support (removed in #478) so users with Anthropic API keys or Claude Code OAuth tokens can use their Claude subscriptions directly. When an Anthropic model is selected and Claude Code credentials are available, the system auto-selects the Claude agent instead of OpenCode. The harness mirrors opentoad's security model: native Bash blocked via --disallowedTools, MCP ShellTool for restricted shell, ASKPASS for git auth. Includes NDJSON streaming, provider error detection, cache/cost tracking, browser skill, and todo progress tracking. Key changes: - action/agents/claude.ts: full Claude Code harness - action/utils/agent.ts: auto-select Claude for anthropic/* models - action/utils/providerErrors.ts: extracted shared provider error detection - action/utils/skills.ts: extracted shared skill installation (agent-aware) - action/models.ts: add CLAUDE_CODE_OAUTH_TOKEN to anthropic envVars - action/utils/docker.ts: add CLAUDE_CODE_OAUTH_TOKEN to test env allowlist - CI: add claude to test matrix, pass CLAUDE_CODE_OAUTH_TOKEN secret Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove unused toolId variable, fix apiKeys test env cleanup The apiKeys test cleanup stripped *_API_KEY vars but missed CLAUDE_CODE_OAUTH_TOKEN which doesn't match that pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: strip provider prefix from PULLFROG_MODEL in Claude agent the env override path was returning the raw value (e.g. "anthropic/claude-sonnet-4-5") without stripping the provider prefix, causing the Claude CLI to receive an invalid model ID. Made-with: Cursor * fix: remove dead cliPath field, add CLAUDE_CODE_OAUTH_TOKEN to workflows remove unused cliPath from Claude agent RunParams, and pass CLAUDE_CODE_OAUTH_TOKEN through all pullfrog.yml workflow templates so users with Claude Pro/Team subscriptions can use their membership. Made-with: Cursor * fix: block Bash subagent in Claude Code disallowedTools Made-with: Cursor * chore: update model snapshot (opencode/openrouter latest → qwen3.6-plus-free) Made-with: Cursor --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
9f566d20e4
commit
0055aef618
@@ -3682,7 +3682,7 @@ var require_util2 = __commonJS({
|
||||
"use strict";
|
||||
var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants2();
|
||||
var { getGlobalOrigin } = require_global();
|
||||
var { performance: performance7 } = __require("perf_hooks");
|
||||
var { performance: performance8 } = __require("perf_hooks");
|
||||
var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util();
|
||||
var assert3 = __require("assert");
|
||||
var { isUint8Array } = __require("util/types");
|
||||
@@ -3845,7 +3845,7 @@ var require_util2 = __commonJS({
|
||||
}
|
||||
}
|
||||
function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
|
||||
return performance7.now();
|
||||
return performance8.now();
|
||||
}
|
||||
function createOpaqueTimingInfo(timingInfo) {
|
||||
return {
|
||||
@@ -56217,7 +56217,7 @@ var require_util11 = __commonJS({
|
||||
var { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require_constants8();
|
||||
var { getGlobalOrigin } = require_global3();
|
||||
var { collectAnHTTPQuotedString, parseMIMEType } = require_data_url();
|
||||
var { performance: performance7 } = __require("node:perf_hooks");
|
||||
var { performance: performance8 } = __require("node:perf_hooks");
|
||||
var { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require_util10();
|
||||
var assert3 = __require("node:assert");
|
||||
var { isUint8Array } = __require("node:util/types");
|
||||
@@ -56372,7 +56372,7 @@ var require_util11 = __commonJS({
|
||||
};
|
||||
}
|
||||
function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
|
||||
return coarsenTime(performance7.now(), crossOriginIsolatedCapability);
|
||||
return coarsenTime(performance8.now(), crossOriginIsolatedCapability);
|
||||
}
|
||||
function createOpaqueTimingInfo(timingInfo) {
|
||||
return {
|
||||
@@ -98890,14 +98890,14 @@ var require_turndown_cjs = __commonJS({
|
||||
} else if (node2.nodeType === 1) {
|
||||
replacement = replacementForNode.call(self2, node2);
|
||||
}
|
||||
return join13(output, replacement);
|
||||
return join14(output, replacement);
|
||||
}, "");
|
||||
}
|
||||
function postProcess(output) {
|
||||
var self2 = this;
|
||||
this.rules.forEach(function(rule) {
|
||||
if (typeof rule.append === "function") {
|
||||
output = join13(output, rule.append(self2.options));
|
||||
output = join14(output, rule.append(self2.options));
|
||||
}
|
||||
});
|
||||
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
||||
@@ -98909,7 +98909,7 @@ var require_turndown_cjs = __commonJS({
|
||||
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
||||
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
||||
}
|
||||
function join13(output, replacement) {
|
||||
function join14(output, replacement) {
|
||||
var s1 = trimTrailingNewlines(output);
|
||||
var s2 = trimLeadingNewlines(replacement);
|
||||
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
||||
@@ -107678,7 +107678,7 @@ function provider(config3) {
|
||||
var providers = {
|
||||
anthropic: provider({
|
||||
displayName: "Anthropic",
|
||||
envVars: ["ANTHROPIC_API_KEY"],
|
||||
envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||
models: {
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
@@ -107934,6 +107934,9 @@ function parseModel(slug) {
|
||||
}
|
||||
return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
|
||||
}
|
||||
function getModelProvider(slug) {
|
||||
return parseModel(slug).provider;
|
||||
}
|
||||
function getModelEnvVars(slug) {
|
||||
const parsed2 = parseModel(slug);
|
||||
const providerConfig = providers[parsed2.provider];
|
||||
@@ -144745,6 +144748,7 @@ var package_default = {
|
||||
turndown: "^7.2.0"
|
||||
},
|
||||
devDependencies: {
|
||||
"@anthropic-ai/claude-code": "2.1.85",
|
||||
"agent-browser": "0.21.0",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"@types/node": "^24.7.2",
|
||||
@@ -149216,9 +149220,8 @@ ${permalinkTip}`
|
||||
}
|
||||
var modes = computeModes();
|
||||
|
||||
// agents/opentoad.ts
|
||||
import { execFileSync as execFileSync2, spawnSync as spawnSync5 } from "node:child_process";
|
||||
import { mkdirSync as mkdirSync3 } from "node:fs";
|
||||
// agents/claude.ts
|
||||
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
||||
import { join as join10 } from "node:path";
|
||||
import { performance as performance6 } from "node:perf_hooks";
|
||||
|
||||
@@ -149307,6 +149310,44 @@ async function installFromNpmTarball(params) {
|
||||
return cliPath;
|
||||
}
|
||||
|
||||
// utils/providerErrors.ts
|
||||
var PROVIDER_ERROR_PATTERNS = [
|
||||
{ pattern: "429", label: "rate limited (429)" },
|
||||
{ pattern: "RESOURCE_EXHAUSTED", label: "quota exhausted" },
|
||||
{ pattern: "quota", label: "quota error" },
|
||||
{ pattern: "status: 500", label: "provider 500 error" },
|
||||
{ pattern: "INTERNAL", label: "provider internal error" },
|
||||
{ pattern: "status: 503", label: "provider unavailable (503)" },
|
||||
{ pattern: "UNAVAILABLE", label: "provider unavailable" },
|
||||
{ pattern: "rate limit", label: "rate limited" },
|
||||
{ pattern: "limit: 0", label: "zero quota" }
|
||||
];
|
||||
function detectProviderError(text) {
|
||||
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
||||
if (text.includes(entry.pattern)) return entry.label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// utils/skills.ts
|
||||
import { spawnSync as spawnSync5 } from "node:child_process";
|
||||
function addSkill(params) {
|
||||
const result = spawnSync5(
|
||||
"npx",
|
||||
["skills", "add", params.ref, "--skill", params.skill, "-g", "-a", params.agent, "-y"],
|
||||
{
|
||||
env: { ...process.env, ...params.env },
|
||||
stdio: "pipe",
|
||||
timeout: 3e4
|
||||
}
|
||||
);
|
||||
if (result.status === 0) {
|
||||
log.info(`installed ${params.skill} skill (${params.agent})`);
|
||||
} else {
|
||||
log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// utils/timer.ts
|
||||
import { performance as performance5 } from "node:perf_hooks";
|
||||
var Timer = class {
|
||||
@@ -149364,7 +149405,360 @@ var agent = (input) => {
|
||||
};
|
||||
};
|
||||
|
||||
// agents/claude.ts
|
||||
async function installClaudeCli() {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "@anthropic-ai/claude-code",
|
||||
version: getDevDependencyVersion("@anthropic-ai/claude-code"),
|
||||
executablePath: "cli.js",
|
||||
installDependencies: false
|
||||
});
|
||||
}
|
||||
function writeMcpConfig(ctx) {
|
||||
const configDir = join10(ctx.tmpdir, ".claude");
|
||||
mkdirSync3(configDir, { recursive: true });
|
||||
const configPath = join10(configDir, "mcp.json");
|
||||
writeFileSync6(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
|
||||
}
|
||||
})
|
||||
);
|
||||
return configPath;
|
||||
}
|
||||
function resolveClaudeModel(modelSlug) {
|
||||
const envModel = process.env.PULLFROG_MODEL?.trim();
|
||||
if (envModel) {
|
||||
const slashIndex = envModel.indexOf("/");
|
||||
const cliModel = slashIndex > 0 ? envModel.slice(slashIndex + 1) : envModel;
|
||||
log.info(`\xBB model: ${cliModel} (override via PULLFROG_MODEL)`);
|
||||
return cliModel;
|
||||
}
|
||||
if (!modelSlug) return void 0;
|
||||
const resolved = resolveModelSlug(modelSlug);
|
||||
if (resolved) {
|
||||
const slashIndex = resolved.indexOf("/");
|
||||
const cliModel = slashIndex > 0 ? resolved.slice(slashIndex + 1) : resolved;
|
||||
log.info(`\xBB model: ${cliModel} (resolved from ${modelSlug})`);
|
||||
return cliModel;
|
||||
}
|
||||
log.warning(`\xBB unknown model slug "${modelSlug}" \u2014 letting Claude Code auto-select`);
|
||||
return void 0;
|
||||
}
|
||||
async function runClaude(params) {
|
||||
const startTime = performance6.now();
|
||||
let eventCount = 0;
|
||||
const thinkingTimer = new ThinkingTimer();
|
||||
let finalOutput = "";
|
||||
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
let costUsd;
|
||||
let tokensLogged = false;
|
||||
function buildUsage() {
|
||||
const totalInput = accumulatedTokens.input + accumulatedTokens.cacheRead + accumulatedTokens.cacheWrite;
|
||||
return totalInput > 0 || accumulatedTokens.output > 0 ? {
|
||||
agent: "claude",
|
||||
inputTokens: totalInput,
|
||||
outputTokens: accumulatedTokens.output,
|
||||
cacheReadTokens: accumulatedTokens.cacheRead || void 0,
|
||||
cacheWriteTokens: accumulatedTokens.cacheWrite || void 0,
|
||||
costUsd
|
||||
} : void 0;
|
||||
}
|
||||
const handlers2 = {
|
||||
system: (_event) => {
|
||||
log.debug(`\xBB ${params.label} system event`);
|
||||
},
|
||||
assistant: (event) => {
|
||||
const content = event.message?.content;
|
||||
if (!content) return;
|
||||
for (const block of content) {
|
||||
if (block.type === "text" && block.text?.trim()) {
|
||||
const message = block.text.trim();
|
||||
log.box(message, { title: params.label });
|
||||
finalOutput = message;
|
||||
} else if (block.type === "tool_use") {
|
||||
const toolName = block.name || "unknown";
|
||||
thinkingTimer.markToolCall();
|
||||
log.toolCall({ toolName, input: block.input || {} });
|
||||
if (toolName.includes("report_progress") && params.todoTracker) {
|
||||
log.debug("\xBB report_progress detected, disabling todo tracking");
|
||||
params.todoTracker.cancel();
|
||||
}
|
||||
if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
|
||||
params.todoTracker.update(block.input);
|
||||
}
|
||||
}
|
||||
}
|
||||
const msgUsage = event.message?.usage;
|
||||
if (msgUsage) {
|
||||
accumulatedTokens.input += msgUsage.input_tokens || 0;
|
||||
accumulatedTokens.output += msgUsage.output_tokens || 0;
|
||||
}
|
||||
},
|
||||
user: (event) => {
|
||||
const content = event.message?.content;
|
||||
if (!content) return;
|
||||
for (const block of content) {
|
||||
if (typeof block === "string") continue;
|
||||
if (block.type === "tool_result") {
|
||||
thinkingTimer.markToolResult();
|
||||
const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
|
||||
(entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
|
||||
).join("\n") : String(block.content);
|
||||
if (block.is_error) {
|
||||
log.info(`\xBB tool error: ${outputContent}`);
|
||||
} else {
|
||||
log.debug(`\xBB tool output: ${outputContent}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
result: (event) => {
|
||||
const subtype = event.subtype || "unknown";
|
||||
const numTurns = event.num_turns || 0;
|
||||
if (subtype === "success") {
|
||||
const usage = event.usage;
|
||||
const inputTokens = usage?.input_tokens || 0;
|
||||
const cacheRead = usage?.cache_read_input_tokens || 0;
|
||||
const cacheWrite = usage?.cache_creation_input_tokens || 0;
|
||||
const outputTokens = usage?.output_tokens || 0;
|
||||
const totalInput = inputTokens + cacheRead + cacheWrite;
|
||||
accumulatedTokens = { input: inputTokens, output: outputTokens, cacheRead, cacheWrite };
|
||||
costUsd = event.total_cost_usd ?? void 0;
|
||||
log.info(
|
||||
`\xBB ${params.label} result: subtype=${subtype}, turns=${numTurns}, cost=$${costUsd?.toFixed(4) ?? "?"}`
|
||||
);
|
||||
if (!tokensLogged) {
|
||||
log.table([
|
||||
[
|
||||
{ data: "Cost", header: true },
|
||||
{ data: "Input", header: true },
|
||||
{ data: "Cache Read", header: true },
|
||||
{ data: "Cache Write", header: true },
|
||||
{ data: "Output", header: true }
|
||||
],
|
||||
[
|
||||
`$${costUsd?.toFixed(4) || "0.0000"}`,
|
||||
String(totalInput),
|
||||
String(cacheRead),
|
||||
String(cacheWrite),
|
||||
String(outputTokens)
|
||||
]
|
||||
]);
|
||||
tokensLogged = true;
|
||||
}
|
||||
} else if (subtype === "error_max_turns") {
|
||||
log.info(`\xBB ${params.label} max turns reached: ${JSON.stringify(event)}`);
|
||||
} else if (subtype === "error_during_execution") {
|
||||
log.info(`\xBB ${params.label} execution error: ${JSON.stringify(event)}`);
|
||||
} else {
|
||||
log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
|
||||
}
|
||||
if (event.result?.trim()) {
|
||||
finalOutput = event.result.trim();
|
||||
}
|
||||
},
|
||||
// additional Claude CLI event types — debug-logged only
|
||||
stream_event: () => {
|
||||
},
|
||||
tool_progress: () => {
|
||||
},
|
||||
tool_use_summary: () => {
|
||||
},
|
||||
auth_status: () => {
|
||||
}
|
||||
};
|
||||
const recentStderr = [];
|
||||
const MAX_STDERR_LINES = 20;
|
||||
let lastProviderError = null;
|
||||
let output = "";
|
||||
let stdoutBuffer = "";
|
||||
try {
|
||||
const result = await spawn({
|
||||
cmd: "node",
|
||||
args: params.args,
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
activityTimeout: 0,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
onStdout: async (chunk) => {
|
||||
const text = chunk.toString();
|
||||
output += text;
|
||||
markActivity();
|
||||
stdoutBuffer += text;
|
||||
const lines = stdoutBuffer.split("\n");
|
||||
stdoutBuffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const event = JSON.parse(trimmed);
|
||||
eventCount++;
|
||||
log.debug(JSON.stringify(event, null, 2));
|
||||
const timeSinceLastActivity = getIdleMs();
|
||||
if (timeSinceLastActivity > 1e4) {
|
||||
log.info(
|
||||
`\xBB no activity for ${(timeSinceLastActivity / 1e3).toFixed(1)}s (${params.label} may be processing internally) (${eventCount} events processed so far)`
|
||||
);
|
||||
}
|
||||
markActivity();
|
||||
const handler2 = handlers2[event.type];
|
||||
if (handler2) {
|
||||
handler2(event);
|
||||
} else {
|
||||
log.debug(`\xBB ${params.label} event (unhandled): type=${event.type}`);
|
||||
}
|
||||
} catch {
|
||||
log.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
onStderr: (chunk) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) return;
|
||||
recentStderr.push(trimmed);
|
||||
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
||||
const providerError = detectProviderError(trimmed);
|
||||
if (providerError) {
|
||||
lastProviderError = providerError;
|
||||
log.info(`\xBB provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
||||
} else {
|
||||
log.debug(trimmed);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (result.exitCode === 0) {
|
||||
await params.todoTracker?.flush();
|
||||
} else {
|
||||
params.todoTracker?.cancel();
|
||||
}
|
||||
const duration4 = performance6.now() - startTime;
|
||||
log.info(
|
||||
`\xBB ${params.label} completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`
|
||||
);
|
||||
if (eventCount === 0) {
|
||||
const stderrContext = recentStderr.join("\n");
|
||||
const diagnosis = lastProviderError ? `provider error: ${lastProviderError}` : "unknown cause (no stdout events received)";
|
||||
log.info(`\xBB ${params.label} produced 0 events (${diagnosis})`);
|
||||
if (stderrContext) log.info(`\xBB last stderr output:
|
||||
${stderrContext}`);
|
||||
}
|
||||
if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0)) {
|
||||
const totalTokens = accumulatedTokens.input + accumulatedTokens.output;
|
||||
log.table([
|
||||
[
|
||||
{ data: "Input Tokens", header: true },
|
||||
{ data: "Output Tokens", header: true },
|
||||
{ data: "Total Tokens", header: true }
|
||||
],
|
||||
[String(accumulatedTokens.input), String(accumulatedTokens.output), String(totalTokens)]
|
||||
]);
|
||||
}
|
||||
const usage = buildUsage();
|
||||
if (result.exitCode !== 0) {
|
||||
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
||||
const errorMessage = result.stderr || result.stdout || `unknown error - no output from Claude CLI${errorContext}`;
|
||||
log.error(
|
||||
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
||||
);
|
||||
log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
|
||||
log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
|
||||
return { success: false, output: finalOutput || output, error: errorMessage, usage };
|
||||
}
|
||||
if (eventCount === 0 && lastProviderError) {
|
||||
return {
|
||||
success: false,
|
||||
output: finalOutput || output,
|
||||
error: `provider error: ${lastProviderError}`,
|
||||
usage
|
||||
};
|
||||
}
|
||||
return { success: true, output: finalOutput || output, usage };
|
||||
} catch (error49) {
|
||||
params.todoTracker?.cancel();
|
||||
const duration4 = performance6.now() - startTime;
|
||||
const errorMessage = error49 instanceof Error ? error49.message : String(error49);
|
||||
const isActivityTimeout = errorMessage.includes("activity timeout");
|
||||
const stderrContext = recentStderr.slice(-10).join("\n");
|
||||
const diagnosis = lastProviderError ? `likely cause: ${lastProviderError}` : eventCount === 0 ? "Claude produced 0 stdout events - check if the API is reachable" : `${eventCount} events were processed before the hang`;
|
||||
log.info(
|
||||
`\xBB ${params.label} ${isActivityTimeout ? "hung" : "failed"} after ${(duration4 / 1e3).toFixed(1)}s: ${errorMessage}`
|
||||
);
|
||||
log.info(`\xBB diagnosis: ${diagnosis}`);
|
||||
if (stderrContext)
|
||||
log.info(
|
||||
`\xBB recent stderr (last ${Math.min(recentStderr.length, 10)} lines):
|
||||
${stderrContext}`
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
output: finalOutput || output,
|
||||
error: `${errorMessage} [${diagnosis}]`,
|
||||
usage: buildUsage()
|
||||
};
|
||||
}
|
||||
}
|
||||
var claude = agent({
|
||||
name: "claude",
|
||||
install: installClaudeCli,
|
||||
run: async (ctx) => {
|
||||
const cliPath = await installClaudeCli();
|
||||
const model = ctx.payload.proxyModel ?? resolveClaudeModel(ctx.payload.model);
|
||||
const homeEnv = {
|
||||
HOME: ctx.tmpdir,
|
||||
XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
|
||||
};
|
||||
mkdirSync3(join10(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
|
||||
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
||||
addSkill({
|
||||
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
||||
skill: "agent-browser",
|
||||
env: homeEnv,
|
||||
agent: "claude"
|
||||
});
|
||||
const mcpConfigPath = writeMcpConfig(ctx);
|
||||
const args2 = [
|
||||
cliPath,
|
||||
"-p",
|
||||
ctx.instructions.full,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--dangerously-skip-permissions",
|
||||
"--mcp-config",
|
||||
mcpConfigPath,
|
||||
"--verbose",
|
||||
"--no-session-persistence",
|
||||
"--disallowedTools",
|
||||
"Bash",
|
||||
"Agent(Bash)"
|
||||
];
|
||||
if (model) {
|
||||
args2.push("--model", model);
|
||||
}
|
||||
const env2 = {
|
||||
...process.env,
|
||||
...homeEnv
|
||||
};
|
||||
const repoDir = process.cwd();
|
||||
log.debug(`\xBB starting Pullfrog (Claude Code): node ${args2.join(" ")}`);
|
||||
log.debug(`\xBB working directory: ${repoDir}`);
|
||||
return runClaude({
|
||||
label: "Pullfrog",
|
||||
args: args2,
|
||||
cwd: repoDir,
|
||||
env: env2,
|
||||
todoTracker: ctx.todoTracker
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// agents/opentoad.ts
|
||||
import { execFileSync as execFileSync2 } from "node:child_process";
|
||||
import { mkdirSync as mkdirSync4 } from "node:fs";
|
||||
import { join as join11 } from "node:path";
|
||||
import { performance as performance7 } from "node:perf_hooks";
|
||||
async function installOpencodeCli() {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "opencode-ai",
|
||||
@@ -149449,41 +149843,8 @@ function resolveOpenCodeModel(ctx) {
|
||||
log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
|
||||
return void 0;
|
||||
}
|
||||
var PROVIDER_ERROR_PATTERNS = [
|
||||
{ pattern: "429", label: "rate limited (429)" },
|
||||
{ pattern: "RESOURCE_EXHAUSTED", label: "quota exhausted" },
|
||||
{ pattern: "quota", label: "quota error" },
|
||||
{ pattern: "status: 500", label: "provider 500 error" },
|
||||
{ pattern: "INTERNAL", label: "provider internal error" },
|
||||
{ pattern: "status: 503", label: "provider unavailable (503)" },
|
||||
{ pattern: "UNAVAILABLE", label: "provider unavailable" },
|
||||
{ pattern: "rate limit", label: "rate limited" },
|
||||
{ pattern: "limit: 0", label: "zero quota" }
|
||||
];
|
||||
function detectProviderError(text) {
|
||||
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
||||
if (text.includes(entry.pattern)) return entry.label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function addSkill(params) {
|
||||
const result = spawnSync5(
|
||||
"npx",
|
||||
["skills", "add", params.ref, "--skill", params.skill, "-g", "-a", "opencode", "-y"],
|
||||
{
|
||||
env: { ...process.env, ...params.env },
|
||||
stdio: "pipe",
|
||||
timeout: 3e4
|
||||
}
|
||||
);
|
||||
if (result.status === 0) {
|
||||
log.info(`installed ${params.skill} skill`);
|
||||
} else {
|
||||
log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`);
|
||||
}
|
||||
}
|
||||
async function runOpenCode(params) {
|
||||
const startTime = performance6.now();
|
||||
const startTime = performance7.now();
|
||||
let eventCount = 0;
|
||||
const thinkingTimer = new ThinkingTimer();
|
||||
let finalOutput = "";
|
||||
@@ -149588,7 +149949,7 @@ async function runOpenCode(params) {
|
||||
if (toolId) {
|
||||
const toolStartTime = toolCallTimings.get(toolId);
|
||||
if (toolStartTime) {
|
||||
const toolDuration = performance6.now() - toolStartTime;
|
||||
const toolDuration = performance7.now() - toolStartTime;
|
||||
toolCallTimings.delete(toolId);
|
||||
const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
|
||||
log.debug(
|
||||
@@ -149708,7 +150069,7 @@ async function runOpenCode(params) {
|
||||
} else {
|
||||
params.todoTracker?.cancel();
|
||||
}
|
||||
const duration4 = performance6.now() - startTime;
|
||||
const duration4 = performance7.now() - startTime;
|
||||
log.info(
|
||||
`\xBB ${params.label} completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`
|
||||
);
|
||||
@@ -149752,7 +150113,7 @@ ${stderrContext}`);
|
||||
return { success: true, output: finalOutput || output, usage };
|
||||
} catch (error49) {
|
||||
params.todoTracker?.cancel();
|
||||
const duration4 = performance6.now() - startTime;
|
||||
const duration4 = performance7.now() - startTime;
|
||||
const errorMessage = error49 instanceof Error ? error49.message : String(error49);
|
||||
const isActivityTimeout = errorMessage.includes("activity timeout");
|
||||
const stderrContext = recentStderr.slice(-10).join("\n");
|
||||
@@ -149785,14 +150146,15 @@ var opentoad = agent({
|
||||
});
|
||||
const homeEnv = {
|
||||
HOME: ctx.tmpdir,
|
||||
XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
|
||||
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
||||
};
|
||||
mkdirSync3(join10(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
||||
mkdirSync4(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
||||
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
||||
addSkill({
|
||||
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
||||
skill: "agent-browser",
|
||||
env: homeEnv
|
||||
env: homeEnv,
|
||||
agent: "opencode"
|
||||
});
|
||||
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||
const env2 = {
|
||||
@@ -149816,10 +150178,35 @@ var opentoad = agent({
|
||||
});
|
||||
|
||||
// agents/index.ts
|
||||
var agents = { opentoad };
|
||||
var agents = { claude, opentoad };
|
||||
|
||||
// utils/agent.ts
|
||||
function resolveAgent() {
|
||||
function hasEnvVar(name) {
|
||||
const val = process.env[name];
|
||||
return typeof val === "string" && val.length > 0;
|
||||
}
|
||||
function hasClaudeCodeAuth() {
|
||||
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
|
||||
}
|
||||
function resolveAgent(ctx) {
|
||||
const envAgent = process.env.PULLFROG_AGENT?.trim();
|
||||
if (envAgent) {
|
||||
if (envAgent in agents) {
|
||||
log.info(`\xBB agent: ${envAgent} (override via PULLFROG_AGENT)`);
|
||||
return agents[envAgent];
|
||||
}
|
||||
log.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
|
||||
}
|
||||
if (ctx?.model) {
|
||||
try {
|
||||
const provider2 = getModelProvider(ctx.model);
|
||||
if (provider2 === "anthropic" && hasClaudeCodeAuth()) {
|
||||
log.info(`\xBB agent: claude (auto-selected for ${ctx.model})`);
|
||||
return agents.claude;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
return agents.opentoad;
|
||||
}
|
||||
|
||||
@@ -149844,7 +150231,7 @@ configure your model at ${settingsUrl}
|
||||
|
||||
for full setup instructions, see https://docs.pullfrog.com/keys`;
|
||||
}
|
||||
function hasEnvVar(name) {
|
||||
function hasEnvVar2(name) {
|
||||
const value2 = process.env[name];
|
||||
return typeof value2 === "string" && value2.length > 0;
|
||||
}
|
||||
@@ -149852,10 +150239,10 @@ function validateAgentApiKey(params) {
|
||||
if (params.model) {
|
||||
const requiredVars = getModelEnvVars(params.model);
|
||||
if (requiredVars.length === 0) return;
|
||||
if (requiredVars.some((v) => hasEnvVar(v))) return;
|
||||
if (requiredVars.some((v) => hasEnvVar2(v))) return;
|
||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||
}
|
||||
const hasAnyKey = [...knownApiKeys].some((k) => hasEnvVar(k));
|
||||
const hasAnyKey = [...knownApiKeys].some((k) => hasEnvVar2(k));
|
||||
if (!hasAnyKey) {
|
||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||
}
|
||||
@@ -149994,9 +150381,9 @@ ${ctx.error}` : ctx.error;
|
||||
|
||||
// utils/gitAuthServer.ts
|
||||
import { randomUUID as randomUUID3 } from "node:crypto";
|
||||
import { writeFileSync as writeFileSync6 } from "node:fs";
|
||||
import { writeFileSync as writeFileSync7 } from "node:fs";
|
||||
import { createServer as createServer2 } from "node:http";
|
||||
import { join as join11 } from "node:path";
|
||||
import { join as join12 } from "node:path";
|
||||
var CODE_TTL_MS = 5 * 60 * 1e3;
|
||||
var TAMPER_WINDOW_MS = 6e4;
|
||||
function revokeGitHubToken(token) {
|
||||
@@ -150068,7 +150455,7 @@ async function startGitAuthServer(tmpdir2) {
|
||||
function writeAskpassScript(code) {
|
||||
const scriptId = randomUUID3();
|
||||
const scriptName = `askpass-${scriptId}.js`;
|
||||
const scriptPath = join11(tmpdir2, scriptName);
|
||||
const scriptPath = join12(tmpdir2, scriptName);
|
||||
const content = [
|
||||
`#!/usr/bin/env node`,
|
||||
`var a=process.argv[2]||"";`,
|
||||
@@ -150083,7 +150470,7 @@ async function startGitAuthServer(tmpdir2) {
|
||||
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
||||
`})}).on("error",function(){process.exit(1)})}`
|
||||
].join("\n");
|
||||
writeFileSync6(scriptPath, content, { mode: 448 });
|
||||
writeFileSync7(scriptPath, content, { mode: 448 });
|
||||
return scriptPath;
|
||||
}
|
||||
async function close() {
|
||||
@@ -150754,9 +151141,9 @@ async function resolveRunContextData(params) {
|
||||
import { execSync as execSync3 } from "node:child_process";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join as join12 } from "node:path";
|
||||
import { join as join13 } from "node:path";
|
||||
function createTempDirectory() {
|
||||
const sharedTempDir = mkdtempSync(join12(tmpdir(), "pullfrog-"));
|
||||
const sharedTempDir = mkdtempSync(join13(tmpdir(), "pullfrog-"));
|
||||
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
||||
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
||||
return sharedTempDir;
|
||||
@@ -151116,7 +151503,7 @@ async function main() {
|
||||
const tmpdir2 = createTempDirectory();
|
||||
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir2), true);
|
||||
setGitAuthServer(gitAuthServer);
|
||||
const agent2 = resolveAgent();
|
||||
const agent2 = resolveAgent({ model: payload.proxyModel ? void 0 : payload.model });
|
||||
validateAgentApiKey({
|
||||
agent: agent2,
|
||||
model: payload.proxyModel ?? payload.model,
|
||||
|
||||
Reference in New Issue
Block a user