Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcb835d129 | |||
| f1400ffb7c | |||
| cd9c3382c7 | |||
| ba1966f17c | |||
| 0055aef618 | |||
| 9f566d20e4 | |||
| 6c5d228c04 |
@@ -37,6 +37,7 @@ jobs:
|
|||||||
# add any additional keys your agent(s) need
|
# add any additional keys your agent(s) need
|
||||||
# optionally, comment out any you won't use
|
# optionally, comment out any you won't use
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
agent: [opentoad]
|
agent: [claude, opentoad]
|
||||||
test:
|
test:
|
||||||
[mcpmerge, nobash, restricted, smoke]
|
[mcpmerge, nobash, restricted, smoke]
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
|
|||||||
@@ -0,0 +1,550 @@
|
|||||||
|
/**
|
||||||
|
* Claude Code agent — secure harness around the `claude` CLI.
|
||||||
|
*
|
||||||
|
* mirrors the opentoad harness's security model:
|
||||||
|
* - native Bash blocked via --disallowedTools (agent cannot shell out)
|
||||||
|
* - MCP ShellTool provides restricted shell (filtered env, no secrets)
|
||||||
|
* - MCP server injected via --mcp-config (not replacing project config)
|
||||||
|
* - ASKPASS handles git auth separately (token never in subprocess env)
|
||||||
|
*
|
||||||
|
* the agent process itself gets full env (needs LLM API keys, PATH, etc.).
|
||||||
|
* security is enforced at the tool layer, not the process layer.
|
||||||
|
*/
|
||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { performance } from "node:perf_hooks";
|
||||||
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
|
import { resolveModelSlug } from "../models.ts";
|
||||||
|
import { getIdleMs, markActivity } from "../utils/activity.ts";
|
||||||
|
import { log } from "../utils/cli.ts";
|
||||||
|
import { installFromNpmTarball } from "../utils/install.ts";
|
||||||
|
import { detectProviderError } from "../utils/providerErrors.ts";
|
||||||
|
import { addSkill } from "../utils/skills.ts";
|
||||||
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
|
import { ThinkingTimer } from "../utils/timer.ts";
|
||||||
|
import type { TodoTracker } from "../utils/todoTracking.ts";
|
||||||
|
import { getDevDependencyVersion } from "../utils/version.ts";
|
||||||
|
import { type AgentResult, type AgentRunContext, type AgentUsage, agent } from "./shared.ts";
|
||||||
|
|
||||||
|
async function installClaudeCli(): Promise<string> {
|
||||||
|
return await installFromNpmTarball({
|
||||||
|
packageName: "@anthropic-ai/claude-code",
|
||||||
|
version: getDevDependencyVersion("@anthropic-ai/claude-code"),
|
||||||
|
executablePath: "cli.js",
|
||||||
|
installDependencies: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── config ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function writeMcpConfig(ctx: AgentRunContext): string {
|
||||||
|
const configDir = join(ctx.tmpdir, ".claude");
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
const configPath = join(configDir, "mcp.json");
|
||||||
|
writeFileSync(
|
||||||
|
configPath,
|
||||||
|
JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── model resolution ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function resolveClaudeModel(modelSlug: string | undefined): string | undefined {
|
||||||
|
// 1. explicit env var override
|
||||||
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
||||||
|
if (envModel) {
|
||||||
|
const slashIndex = envModel.indexOf("/");
|
||||||
|
const cliModel = slashIndex > 0 ? envModel.slice(slashIndex + 1) : envModel;
|
||||||
|
log.info(`» model: ${cliModel} (override via PULLFROG_MODEL)`);
|
||||||
|
return cliModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modelSlug) return undefined;
|
||||||
|
|
||||||
|
// 2. resolve slug to concrete specifier (e.g. "anthropic/claude-opus" → "anthropic/claude-opus-4-6")
|
||||||
|
// then strip the "anthropic/" prefix to get the Claude CLI model name
|
||||||
|
const resolved = resolveModelSlug(modelSlug);
|
||||||
|
if (resolved) {
|
||||||
|
const slashIndex = resolved.indexOf("/");
|
||||||
|
const cliModel = slashIndex > 0 ? resolved.slice(slashIndex + 1) : resolved;
|
||||||
|
log.info(`» model: ${cliModel} (resolved from ${modelSlug})`);
|
||||||
|
return cliModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warning(`» unknown model slug "${modelSlug}" — letting Claude Code auto-select`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NDJSON event types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ContentBlock {
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
input?: unknown;
|
||||||
|
tool_use_id?: string;
|
||||||
|
content?: string | unknown;
|
||||||
|
is_error?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClaudeSystemEvent {
|
||||||
|
type: "system";
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClaudeAssistantEvent {
|
||||||
|
type: "assistant";
|
||||||
|
message?: {
|
||||||
|
role?: string;
|
||||||
|
content?: ContentBlock[];
|
||||||
|
model?: string;
|
||||||
|
usage?: {
|
||||||
|
input_tokens?: number;
|
||||||
|
output_tokens?: number;
|
||||||
|
cache_creation_input_tokens?: number;
|
||||||
|
cache_read_input_tokens?: number;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClaudeUserEvent {
|
||||||
|
type: "user";
|
||||||
|
message?: {
|
||||||
|
role?: string;
|
||||||
|
content?: ContentBlock[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClaudeResultEvent {
|
||||||
|
type: "result";
|
||||||
|
subtype?: string;
|
||||||
|
result?: string;
|
||||||
|
session_id?: string;
|
||||||
|
num_turns?: number;
|
||||||
|
total_cost_usd?: number;
|
||||||
|
total_input_tokens?: number;
|
||||||
|
total_output_tokens?: number;
|
||||||
|
usage?: {
|
||||||
|
input_tokens?: number;
|
||||||
|
output_tokens?: number;
|
||||||
|
cache_read_input_tokens?: number;
|
||||||
|
cache_creation_input_tokens?: number;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// additional event types emitted by Claude CLI (handled as no-ops / debug)
|
||||||
|
interface ClaudeStreamEvent {
|
||||||
|
type: "stream_event";
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
interface ClaudeToolProgressEvent {
|
||||||
|
type: "tool_progress";
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
interface ClaudeToolUseSummaryEvent {
|
||||||
|
type: "tool_use_summary";
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
interface ClaudeAuthStatusEvent {
|
||||||
|
type: "auth_status";
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeEvent =
|
||||||
|
| ClaudeSystemEvent
|
||||||
|
| ClaudeAssistantEvent
|
||||||
|
| ClaudeUserEvent
|
||||||
|
| ClaudeResultEvent
|
||||||
|
| ClaudeStreamEvent
|
||||||
|
| ClaudeToolProgressEvent
|
||||||
|
| ClaudeToolUseSummaryEvent
|
||||||
|
| ClaudeAuthStatusEvent;
|
||||||
|
|
||||||
|
// ── runner ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type RunParams = {
|
||||||
|
label: string;
|
||||||
|
args: string[];
|
||||||
|
cwd: string;
|
||||||
|
env: Record<string, string | undefined>;
|
||||||
|
todoTracker?: TodoTracker | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function runClaude(params: RunParams): Promise<AgentResult> {
|
||||||
|
const startTime = performance.now();
|
||||||
|
let eventCount = 0;
|
||||||
|
const thinkingTimer = new ThinkingTimer();
|
||||||
|
|
||||||
|
let finalOutput = "";
|
||||||
|
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||||
|
let tokensLogged = false;
|
||||||
|
|
||||||
|
function buildUsage(): AgentUsage | undefined {
|
||||||
|
const totalInput =
|
||||||
|
accumulatedTokens.input + accumulatedTokens.cacheRead + accumulatedTokens.cacheWrite;
|
||||||
|
return totalInput > 0 || accumulatedTokens.output > 0
|
||||||
|
? {
|
||||||
|
agent: "claude",
|
||||||
|
inputTokens: totalInput,
|
||||||
|
outputTokens: accumulatedTokens.output,
|
||||||
|
cacheReadTokens: accumulatedTokens.cacheRead || undefined,
|
||||||
|
cacheWriteTokens: accumulatedTokens.cacheWrite || undefined,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
system: (_event: ClaudeSystemEvent) => {
|
||||||
|
log.debug(`» ${params.label} system event`);
|
||||||
|
},
|
||||||
|
assistant: (event: ClaudeAssistantEvent) => {
|
||||||
|
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 || {} });
|
||||||
|
|
||||||
|
// agent's explicit MCP report_progress takes priority over todo tracking
|
||||||
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
||||||
|
log.debug("» report_progress detected, disabling todo tracking");
|
||||||
|
params.todoTracker.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse TodoWrite events for live progress tracking
|
||||||
|
if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
|
||||||
|
params.todoTracker.update(block.input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulate per-message usage if available
|
||||||
|
const msgUsage = event.message?.usage;
|
||||||
|
if (msgUsage) {
|
||||||
|
accumulatedTokens.input += msgUsage.input_tokens || 0;
|
||||||
|
accumulatedTokens.output += msgUsage.output_tokens || 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: (event: ClaudeUserEvent) => {
|
||||||
|
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 as unknown[])
|
||||||
|
.map((entry: unknown) =>
|
||||||
|
typeof entry === "string"
|
||||||
|
? entry
|
||||||
|
: typeof entry === "object" && entry !== null && "text" in entry
|
||||||
|
? String((entry as { text: unknown }).text)
|
||||||
|
: JSON.stringify(entry)
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
: String(block.content);
|
||||||
|
|
||||||
|
if (block.is_error) {
|
||||||
|
log.info(`» tool error: ${outputContent}`);
|
||||||
|
} else {
|
||||||
|
log.debug(`» tool output: ${outputContent}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result: (event: ClaudeResultEvent) => {
|
||||||
|
const subtype = event.subtype || "unknown";
|
||||||
|
const numTurns = event.num_turns || 0;
|
||||||
|
|
||||||
|
if (subtype === "success") {
|
||||||
|
// extract detailed usage from result event (most accurate source)
|
||||||
|
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 };
|
||||||
|
|
||||||
|
log.info(`» ${params.label} result: subtype=${subtype}, turns=${numTurns}`);
|
||||||
|
|
||||||
|
if (!tokensLogged) {
|
||||||
|
log.table([
|
||||||
|
[
|
||||||
|
{ data: "Input", header: true },
|
||||||
|
{ data: "Cache Read", header: true },
|
||||||
|
{ data: "Cache Write", header: true },
|
||||||
|
{ data: "Output", header: true },
|
||||||
|
],
|
||||||
|
[String(totalInput), String(cacheRead), String(cacheWrite), String(outputTokens)],
|
||||||
|
]);
|
||||||
|
tokensLogged = true;
|
||||||
|
}
|
||||||
|
} else if (subtype === "error_max_turns") {
|
||||||
|
log.info(`» ${params.label} max turns reached: ${JSON.stringify(event)}`);
|
||||||
|
} else if (subtype === "error_during_execution") {
|
||||||
|
log.info(`» ${params.label} execution error: ${JSON.stringify(event)}`);
|
||||||
|
} else {
|
||||||
|
log.info(`» ${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: string[] = [];
|
||||||
|
const MAX_STDERR_LINES = 20;
|
||||||
|
let lastProviderError: string | null = 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) as ClaudeEvent;
|
||||||
|
eventCount++;
|
||||||
|
log.debug(JSON.stringify(event, null, 2));
|
||||||
|
|
||||||
|
const timeSinceLastActivity = getIdleMs();
|
||||||
|
if (timeSinceLastActivity > 10000) {
|
||||||
|
log.info(
|
||||||
|
`» no activity for ${(timeSinceLastActivity / 1000).toFixed(1)}s (${params.label} may be processing internally) (${eventCount} events processed so far)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
markActivity();
|
||||||
|
const handler = handlers[event.type as keyof typeof handlers];
|
||||||
|
if (handler) {
|
||||||
|
(handler as (e: ClaudeEvent) => void)(event);
|
||||||
|
} else {
|
||||||
|
log.debug(`» ${params.label} event (unhandled): type=${event.type}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log.debug(`» 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(`» 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 duration = performance.now() - startTime;
|
||||||
|
log.info(
|
||||||
|
`» ${params.label} completed in ${Math.round(duration)}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(`» ${params.label} produced 0 events (${diagnosis})`);
|
||||||
|
if (stderrContext) log.info(`» last stderr output:\n${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 (error) {
|
||||||
|
params.todoTracker?.cancel();
|
||||||
|
const duration = performance.now() - startTime;
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
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(
|
||||||
|
`» ${params.label} ${isActivityTimeout ? "hung" : "failed"} after ${(duration / 1000).toFixed(1)}s: ${errorMessage}`
|
||||||
|
);
|
||||||
|
log.info(`» diagnosis: ${diagnosis}`);
|
||||||
|
if (stderrContext)
|
||||||
|
log.info(
|
||||||
|
`» recent stderr (last ${Math.min(recentStderr.length, 10)} lines):\n${stderrContext}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: finalOutput || output,
|
||||||
|
error: `${errorMessage} [${diagnosis}]`,
|
||||||
|
usage: buildUsage(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── agent ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const 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: join(ctx.tmpdir, ".config"),
|
||||||
|
};
|
||||||
|
|
||||||
|
mkdirSync(join(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 args = [
|
||||||
|
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) {
|
||||||
|
args.push("--model", model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent process gets full env — needs LLM API keys, PATH, locale, etc.
|
||||||
|
// security is enforced via --disallowedTools (Bash + Bash subagent) and MCP tool filtering.
|
||||||
|
const env: Record<string, string | undefined> = {
|
||||||
|
...process.env,
|
||||||
|
...homeEnv,
|
||||||
|
};
|
||||||
|
|
||||||
|
const repoDir = process.cwd();
|
||||||
|
|
||||||
|
log.debug(`» starting Pullfrog (Claude Code): node ${args.join(" ")}`);
|
||||||
|
log.debug(`» working directory: ${repoDir}`);
|
||||||
|
|
||||||
|
return runClaude({
|
||||||
|
label: "Pullfrog",
|
||||||
|
args,
|
||||||
|
cwd: repoDir,
|
||||||
|
env,
|
||||||
|
todoTracker: ctx.todoTracker,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
|
import { claude } from "./claude.ts";
|
||||||
import { opentoad } from "./opentoad.ts";
|
import { opentoad } from "./opentoad.ts";
|
||||||
import type { Agent } from "./shared.ts";
|
import type { Agent } from "./shared.ts";
|
||||||
|
|
||||||
export type { Agent, AgentUsage } from "./shared.ts";
|
export type { Agent, AgentUsage } from "./shared.ts";
|
||||||
|
|
||||||
export const agents = { opentoad } satisfies Record<string, Agent>;
|
export const agents = { claude, opentoad } satisfies Record<string, Agent>;
|
||||||
|
|||||||
+6
-41
@@ -10,7 +10,7 @@
|
|||||||
* the agent process itself gets full env (needs LLM API keys, PATH, etc.).
|
* the agent process itself gets full env (needs LLM API keys, PATH, etc.).
|
||||||
* security is enforced at the tool layer, not the process layer.
|
* security is enforced at the tool layer, not the process layer.
|
||||||
*/
|
*/
|
||||||
import { execFileSync, spawnSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { performance } from "node:perf_hooks";
|
import { performance } from "node:perf_hooks";
|
||||||
@@ -19,6 +19,8 @@ import { modelAliases, resolveCliModel } from "../models.ts";
|
|||||||
import { getIdleMs, markActivity } from "../utils/activity.ts";
|
import { getIdleMs, markActivity } from "../utils/activity.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { installFromNpmTarball } from "../utils/install.ts";
|
import { installFromNpmTarball } from "../utils/install.ts";
|
||||||
|
import { detectProviderError } from "../utils/providerErrors.ts";
|
||||||
|
import { addSkill } from "../utils/skills.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
import { ThinkingTimer } from "../utils/timer.ts";
|
import { ThinkingTimer } from "../utils/timer.ts";
|
||||||
import type { TodoTracker } from "../utils/todoTracking.ts";
|
import type { TodoTracker } from "../utils/todoTracking.ts";
|
||||||
@@ -52,7 +54,7 @@ function buildSecurityConfig(ctx: AgentRunContext, model: string | undefined): s
|
|||||||
edit: "allow",
|
edit: "allow",
|
||||||
read: "allow",
|
read: "allow",
|
||||||
webfetch: "allow",
|
webfetch: "allow",
|
||||||
external_directory: "deny",
|
external_directory: "allow",
|
||||||
skill: "allow",
|
skill: "allow",
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
@@ -153,44 +155,6 @@ function resolveOpenCodeModel(ctx: {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── provider error detection ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const 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: string): string | null {
|
|
||||||
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
|
||||||
if (text.includes(entry.pattern)) return entry.label;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSkill(params: { ref: string; skill: string; env: Record<string, string> }): void {
|
|
||||||
const result = spawnSync(
|
|
||||||
"npx",
|
|
||||||
["skills", "add", params.ref, "--skill", params.skill, "-g", "-a", "opencode", "-y"],
|
|
||||||
{
|
|
||||||
env: { ...process.env, ...params.env },
|
|
||||||
stdio: "pipe",
|
|
||||||
timeout: 30_000,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (result.status === 0) {
|
|
||||||
log.info(`installed ${params.skill} skill`);
|
|
||||||
} else {
|
|
||||||
log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── NDJSON event types ─────────────────────────────────────────────────────────
|
// ── NDJSON event types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface OpenCodeInitEvent {
|
interface OpenCodeInitEvent {
|
||||||
@@ -502,7 +466,7 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
|
|||||||
args: params.args,
|
args: params.args,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
env: params.env,
|
env: params.env,
|
||||||
activityTimeout: 0,
|
activityTimeout: 300_000,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
onStdout: async (chunk) => {
|
onStdout: async (chunk) => {
|
||||||
const text = chunk.toString();
|
const text = chunk.toString();
|
||||||
@@ -680,6 +644,7 @@ export const opentoad = agent({
|
|||||||
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
||||||
skill: "agent-browser",
|
skill: "agent-browser",
|
||||||
env: homeEnv,
|
env: homeEnv,
|
||||||
|
agent: "opencode",
|
||||||
});
|
});
|
||||||
|
|
||||||
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||||
|
|||||||
@@ -3682,7 +3682,7 @@ var require_util2 = __commonJS({
|
|||||||
"use strict";
|
"use strict";
|
||||||
var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants2();
|
var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants2();
|
||||||
var { getGlobalOrigin } = require_global();
|
var { getGlobalOrigin } = require_global();
|
||||||
var { performance: performance7 } = __require("perf_hooks");
|
var { performance: performance8 } = __require("perf_hooks");
|
||||||
var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util();
|
var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util();
|
||||||
var assert3 = __require("assert");
|
var assert3 = __require("assert");
|
||||||
var { isUint8Array } = __require("util/types");
|
var { isUint8Array } = __require("util/types");
|
||||||
@@ -3845,7 +3845,7 @@ var require_util2 = __commonJS({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
|
function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
|
||||||
return performance7.now();
|
return performance8.now();
|
||||||
}
|
}
|
||||||
function createOpaqueTimingInfo(timingInfo) {
|
function createOpaqueTimingInfo(timingInfo) {
|
||||||
return {
|
return {
|
||||||
@@ -56217,7 +56217,7 @@ var require_util11 = __commonJS({
|
|||||||
var { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require_constants8();
|
var { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require_constants8();
|
||||||
var { getGlobalOrigin } = require_global3();
|
var { getGlobalOrigin } = require_global3();
|
||||||
var { collectAnHTTPQuotedString, parseMIMEType } = require_data_url();
|
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 { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require_util10();
|
||||||
var assert3 = __require("node:assert");
|
var assert3 = __require("node:assert");
|
||||||
var { isUint8Array } = __require("node:util/types");
|
var { isUint8Array } = __require("node:util/types");
|
||||||
@@ -56372,7 +56372,7 @@ var require_util11 = __commonJS({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
|
function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
|
||||||
return coarsenTime(performance7.now(), crossOriginIsolatedCapability);
|
return coarsenTime(performance8.now(), crossOriginIsolatedCapability);
|
||||||
}
|
}
|
||||||
function createOpaqueTimingInfo(timingInfo) {
|
function createOpaqueTimingInfo(timingInfo) {
|
||||||
return {
|
return {
|
||||||
@@ -98890,14 +98890,14 @@ var require_turndown_cjs = __commonJS({
|
|||||||
} else if (node2.nodeType === 1) {
|
} else if (node2.nodeType === 1) {
|
||||||
replacement = replacementForNode.call(self2, node2);
|
replacement = replacementForNode.call(self2, node2);
|
||||||
}
|
}
|
||||||
return join13(output, replacement);
|
return join14(output, replacement);
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
function postProcess(output) {
|
function postProcess(output) {
|
||||||
var self2 = this;
|
var self2 = this;
|
||||||
this.rules.forEach(function(rule) {
|
this.rules.forEach(function(rule) {
|
||||||
if (typeof rule.append === "function") {
|
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]+$/, "");
|
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();
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
||||||
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
||||||
}
|
}
|
||||||
function join13(output, replacement) {
|
function join14(output, replacement) {
|
||||||
var s1 = trimTrailingNewlines(output);
|
var s1 = trimTrailingNewlines(output);
|
||||||
var s2 = trimLeadingNewlines(replacement);
|
var s2 = trimLeadingNewlines(replacement);
|
||||||
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
||||||
@@ -107596,27 +107596,21 @@ function formatJsonValue(value2) {
|
|||||||
}
|
}
|
||||||
function formatUsageSummary(entries) {
|
function formatUsageSummary(entries) {
|
||||||
if (entries.length === 0) return "";
|
if (entries.length === 0) return "";
|
||||||
const hasCost = entries.some((e) => e.costUsd !== void 0);
|
const header = "| Agent | Input | Output | Cache Read | Cache Write |";
|
||||||
const header = hasCost ? "| Agent | Input | Output | Cache Read | Cache Write | Cost |" : "| Agent | Input | Output | Cache Read | Cache Write |";
|
const separatorRow = "| --- | ---: | ---: | ---: | ---: |";
|
||||||
const fmt = (n) => n.toLocaleString("en-US");
|
const fmt = (n) => n.toLocaleString("en-US");
|
||||||
const separatorRow = hasCost ? "| --- | ---: | ---: | ---: | ---: | ---: |" : "| --- | ---: | ---: | ---: | ---: |";
|
const rows = entries.map(
|
||||||
const rows = entries.map((e) => {
|
(e) => `| ${e.agent} | ${fmt(e.inputTokens)} | ${fmt(e.outputTokens)} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} |`
|
||||||
const base = `| ${e.agent} | ${fmt(e.inputTokens)} | ${fmt(e.outputTokens)} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} |`;
|
);
|
||||||
return hasCost ? `${base} ${e.costUsd !== void 0 ? `$${e.costUsd.toFixed(4)}` : "-"} |` : base;
|
|
||||||
});
|
|
||||||
const totalsRows = [];
|
const totalsRows = [];
|
||||||
if (entries.length > 1) {
|
if (entries.length > 1) {
|
||||||
const totalInput = entries.reduce((sum, e) => sum + e.inputTokens, 0);
|
const totalInput = entries.reduce((sum, e) => sum + e.inputTokens, 0);
|
||||||
const totalOutput = entries.reduce((sum, e) => sum + e.outputTokens, 0);
|
const totalOutput = entries.reduce((sum, e) => sum + e.outputTokens, 0);
|
||||||
const totalCacheRead = entries.reduce((sum, e) => sum + (e.cacheReadTokens ?? 0), 0);
|
const totalCacheRead = entries.reduce((sum, e) => sum + (e.cacheReadTokens ?? 0), 0);
|
||||||
const totalCacheWrite = entries.reduce((sum, e) => sum + (e.cacheWriteTokens ?? 0), 0);
|
const totalCacheWrite = entries.reduce((sum, e) => sum + (e.cacheWriteTokens ?? 0), 0);
|
||||||
const totalBase = `| **Total** | **${fmt(totalInput)}** | **${fmt(totalOutput)}** | **${fmt(totalCacheRead)}** | **${fmt(totalCacheWrite)}** |`;
|
totalsRows.push(
|
||||||
if (hasCost) {
|
`| **Total** | **${fmt(totalInput)}** | **${fmt(totalOutput)}** | **${fmt(totalCacheRead)}** | **${fmt(totalCacheWrite)}** |`
|
||||||
const totalCost = entries.reduce((sum, e) => sum + (e.costUsd ?? 0), 0);
|
);
|
||||||
totalsRows.push(`${totalBase} **$${totalCost.toFixed(4)}** |`);
|
|
||||||
} else {
|
|
||||||
totalsRows.push(totalBase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
"<details>",
|
"<details>",
|
||||||
@@ -107678,7 +107672,7 @@ function provider(config3) {
|
|||||||
var providers = {
|
var providers = {
|
||||||
anthropic: provider({
|
anthropic: provider({
|
||||||
displayName: "Anthropic",
|
displayName: "Anthropic",
|
||||||
envVars: ["ANTHROPIC_API_KEY"],
|
envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||||
models: {
|
models: {
|
||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
@@ -107721,7 +107715,7 @@ var providers = {
|
|||||||
}),
|
}),
|
||||||
google: provider({
|
google: provider({
|
||||||
displayName: "Google",
|
displayName: "Google",
|
||||||
envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
|
envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
"gemini-pro": {
|
"gemini-pro": {
|
||||||
displayName: "Gemini Pro",
|
displayName: "Gemini Pro",
|
||||||
@@ -107934,6 +107928,9 @@ function parseModel(slug) {
|
|||||||
}
|
}
|
||||||
return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
|
return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
|
||||||
}
|
}
|
||||||
|
function getModelProvider(slug) {
|
||||||
|
return parseModel(slug).provider;
|
||||||
|
}
|
||||||
function getModelEnvVars(slug) {
|
function getModelEnvVars(slug) {
|
||||||
const parsed2 = parseModel(slug);
|
const parsed2 = parseModel(slug);
|
||||||
const providerConfig = providers[parsed2.provider];
|
const providerConfig = providers[parsed2.provider];
|
||||||
@@ -144699,7 +144696,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.184",
|
version: "0.0.186",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -144745,6 +144742,7 @@ var package_default = {
|
|||||||
turndown: "^7.2.0"
|
turndown: "^7.2.0"
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
|
"@anthropic-ai/claude-code": "2.1.85",
|
||||||
"agent-browser": "0.21.0",
|
"agent-browser": "0.21.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@types/node": "^24.7.2",
|
"@types/node": "^24.7.2",
|
||||||
@@ -145007,20 +145005,19 @@ function installSignalHandler() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function spawn(options) {
|
async function spawn(options) {
|
||||||
const { cmd, args: args2, env: env2, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
|
||||||
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||||
installSignalHandler();
|
installSignalHandler();
|
||||||
const startTime = performance3.now();
|
const startTime = performance3.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
let stderrBuffer = "";
|
let stderrBuffer = "";
|
||||||
return new Promise((resolve2, reject) => {
|
return new Promise((resolve2, reject) => {
|
||||||
const child = nodeSpawn(cmd, args2, {
|
const child = nodeSpawn(options.cmd, options.args, {
|
||||||
env: env2 || {
|
env: options.env || {
|
||||||
PATH: process.env.PATH || "",
|
PATH: process.env.PATH || "",
|
||||||
HOME: process.env.HOME || ""
|
HOME: process.env.HOME || ""
|
||||||
},
|
},
|
||||||
stdio: stdio || ["pipe", "pipe", "pipe"],
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
||||||
cwd: cwd || process.cwd()
|
cwd: options.cwd || process.cwd()
|
||||||
});
|
});
|
||||||
trackChild({ child });
|
trackChild({ child });
|
||||||
let timeoutId;
|
let timeoutId;
|
||||||
@@ -145028,7 +145025,7 @@ async function spawn(options) {
|
|||||||
let isTimedOut = false;
|
let isTimedOut = false;
|
||||||
let isActivityTimedOut = false;
|
let isActivityTimedOut = false;
|
||||||
let lastActivityTime = performance3.now();
|
let lastActivityTime = performance3.now();
|
||||||
if (timeout) {
|
if (options.timeout) {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
isTimedOut = true;
|
isTimedOut = true;
|
||||||
child.kill("SIGTERM");
|
child.kill("SIGTERM");
|
||||||
@@ -145037,10 +145034,12 @@ async function spawn(options) {
|
|||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
}, 5e3);
|
}, 5e3);
|
||||||
}, timeout);
|
}, options.timeout);
|
||||||
}
|
}
|
||||||
if (activityTimeoutMs > 0) {
|
if (activityTimeoutMs > 0) {
|
||||||
log.debug(`spawn activity timer: pid=${child.pid} cmd=${cmd} timeout=${activityTimeoutMs}ms`);
|
log.debug(
|
||||||
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
||||||
|
);
|
||||||
activityCheckIntervalId = setInterval(() => {
|
activityCheckIntervalId = setInterval(() => {
|
||||||
const idleMs = performance3.now() - lastActivityTime;
|
const idleMs = performance3.now() - lastActivityTime;
|
||||||
log.debug(
|
log.debug(
|
||||||
@@ -145049,7 +145048,9 @@ async function spawn(options) {
|
|||||||
if (idleMs > activityTimeoutMs) {
|
if (idleMs > activityTimeoutMs) {
|
||||||
isActivityTimedOut = true;
|
isActivityTimedOut = true;
|
||||||
const idleSec = Math.round(idleMs / 1e3);
|
const idleSec = Math.round(idleMs / 1e3);
|
||||||
log.info(`no output for ${idleSec}s from pid=${child.pid} (${cmd}), killing process`);
|
log.info(
|
||||||
|
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
||||||
|
);
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
clearInterval(activityCheckIntervalId);
|
clearInterval(activityCheckIntervalId);
|
||||||
}
|
}
|
||||||
@@ -145063,15 +145064,14 @@ async function spawn(options) {
|
|||||||
updateActivity();
|
updateActivity();
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
stdoutBuffer += chunk;
|
stdoutBuffer += chunk;
|
||||||
onStdout?.(chunk);
|
options.onStdout?.(chunk);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (child.stderr) {
|
if (child.stderr) {
|
||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
updateActivity();
|
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
stderrBuffer += chunk;
|
stderrBuffer += chunk;
|
||||||
onStderr?.(chunk);
|
options.onStderr?.(chunk);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
child.on("close", (exitCode) => {
|
child.on("close", (exitCode) => {
|
||||||
@@ -145080,7 +145080,7 @@ async function spawn(options) {
|
|||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
||||||
if (isTimedOut) {
|
if (isTimedOut) {
|
||||||
reject(new Error(`process timed out after ${timeout}ms`));
|
reject(new Error(`process timed out after ${options.timeout}ms`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isActivityTimedOut) {
|
if (isActivityTimedOut) {
|
||||||
@@ -145108,8 +145108,8 @@ async function spawn(options) {
|
|||||||
durationMs
|
durationMs
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (input && child.stdin && stdio?.[0] !== "ignore") {
|
if (options.input && child.stdin && options.stdio?.[0] !== "ignore") {
|
||||||
child.stdin.write(input);
|
child.stdin.write(options.input);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -148198,7 +148198,8 @@ ${learningsStep(6)}`,
|
|||||||
|
|
||||||
6. Submit:
|
6. Submit:
|
||||||
- **actionable issues found**: call \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, all comments, and an **empty body** \u2014 inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary.
|
- **actionable issues found**: call \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, all comments, and an **empty body** \u2014 inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary.
|
||||||
- **no actionable issues found**: do NOT submit a review. Call \`${ghPullfrogMcpName}/report_progress\` with a brief note (e.g., "Re-reviewed \u2014 no new issues found.").`,
|
- **no actionable issues, but substantive changes or prior fixes confirmed**: post a brief comment (1-3 sentences) via \`${ghPullfrogMcpName}/create_issue_comment\` confirming the review happened and listing which prior review issues were resolved. Substantive = new functionality, behavior changes, architectural changes, or fixes to previously flagged issues.
|
||||||
|
- **no actionable issues, non-substantive changes only** (e.g., trivial formatting, import reordering, comment tweaks with no functional impact): do NOT submit a review. Call \`${ghPullfrogMcpName}/report_progress\` with a brief note (e.g., "Re-reviewed \u2014 no new issues found.").`,
|
||||||
Plan: `### Checklist
|
Plan: `### Checklist
|
||||||
|
|
||||||
1. Analyze the task and gather context:
|
1. Analyze the task and gather context:
|
||||||
@@ -149048,7 +149049,8 @@ ${permalinkTip}
|
|||||||
|
|
||||||
7. **SUBMIT** \u2014 Determine whether to submit a review:
|
7. **SUBMIT** \u2014 Determine whether to submit a review:
|
||||||
- **Issues found**: Submit via ${ghPullfrogMcpName}/create_pull_request_review with \`approved: false\`, the inline comments from step 6, and an **empty body** \u2014 inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary (e.g., "Re-reviewed \u2014 found 2 issues in the new commits.").
|
- **Issues found**: Submit via ${ghPullfrogMcpName}/create_pull_request_review with \`approved: false\`, the inline comments from step 6, and an **empty body** \u2014 inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary (e.g., "Re-reviewed \u2014 found 2 issues in the new commits.").
|
||||||
- **No issues found**: Do NOT submit a review. Call \`report_progress\` with a brief note (e.g., "Re-reviewed \u2014 no new issues found.").
|
- **No issues, but substantive changes or prior fixes confirmed**: Post a brief comment (1-3 sentences) via ${ghPullfrogMcpName}/create_issue_comment confirming the review happened and listing which prior review issues were resolved. Substantive = new functionality, behavior changes, architectural changes, or fixes to previously flagged issues.
|
||||||
|
- **No issues, non-substantive changes only** (e.g., trivial formatting, import reordering, comment tweaks with no functional impact): Do NOT submit a review. Call \`report_progress\` with a brief note (e.g., "Re-reviewed \u2014 no new issues found.").
|
||||||
|
|
||||||
${permalinkTip}
|
${permalinkTip}
|
||||||
`
|
`
|
||||||
@@ -149212,9 +149214,8 @@ ${permalinkTip}`
|
|||||||
}
|
}
|
||||||
var modes = computeModes();
|
var modes = computeModes();
|
||||||
|
|
||||||
// agents/opentoad.ts
|
// agents/claude.ts
|
||||||
import { execFileSync as execFileSync2, spawnSync as spawnSync5 } from "node:child_process";
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
||||||
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
||||||
import { join as join10 } from "node:path";
|
import { join as join10 } from "node:path";
|
||||||
import { performance as performance6 } from "node:perf_hooks";
|
import { performance as performance6 } from "node:perf_hooks";
|
||||||
|
|
||||||
@@ -149303,6 +149304,44 @@ async function installFromNpmTarball(params) {
|
|||||||
return cliPath;
|
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
|
// utils/timer.ts
|
||||||
import { performance as performance5 } from "node:perf_hooks";
|
import { performance as performance5 } from "node:perf_hooks";
|
||||||
var Timer = class {
|
var Timer = class {
|
||||||
@@ -149360,7 +149399,348 @@ 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 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
|
||||||
|
} : 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 };
|
||||||
|
log.info(`\xBB ${params.label} result: subtype=${subtype}, turns=${numTurns}`);
|
||||||
|
if (!tokensLogged) {
|
||||||
|
log.table([
|
||||||
|
[
|
||||||
|
{ data: "Input", header: true },
|
||||||
|
{ data: "Cache Read", header: true },
|
||||||
|
{ data: "Cache Write", header: true },
|
||||||
|
{ data: "Output", header: true }
|
||||||
|
],
|
||||||
|
[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
|
// 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() {
|
async function installOpencodeCli() {
|
||||||
return await installFromNpmTarball({
|
return await installFromNpmTarball({
|
||||||
packageName: "opencode-ai",
|
packageName: "opencode-ai",
|
||||||
@@ -149376,7 +149756,7 @@ function buildSecurityConfig(ctx, model) {
|
|||||||
edit: "allow",
|
edit: "allow",
|
||||||
read: "allow",
|
read: "allow",
|
||||||
webfetch: "allow",
|
webfetch: "allow",
|
||||||
external_directory: "deny",
|
external_directory: "allow",
|
||||||
skill: "allow"
|
skill: "allow"
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
@@ -149445,41 +149825,8 @@ function resolveOpenCodeModel(ctx) {
|
|||||||
log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
|
log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
|
||||||
return void 0;
|
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) {
|
async function runOpenCode(params) {
|
||||||
const startTime = performance6.now();
|
const startTime = performance7.now();
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
const thinkingTimer = new ThinkingTimer();
|
const thinkingTimer = new ThinkingTimer();
|
||||||
let finalOutput = "";
|
let finalOutput = "";
|
||||||
@@ -149584,7 +149931,7 @@ async function runOpenCode(params) {
|
|||||||
if (toolId) {
|
if (toolId) {
|
||||||
const toolStartTime = toolCallTimings.get(toolId);
|
const toolStartTime = toolCallTimings.get(toolId);
|
||||||
if (toolStartTime) {
|
if (toolStartTime) {
|
||||||
const toolDuration = performance6.now() - toolStartTime;
|
const toolDuration = performance7.now() - toolStartTime;
|
||||||
toolCallTimings.delete(toolId);
|
toolCallTimings.delete(toolId);
|
||||||
const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
|
const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
|
||||||
log.debug(
|
log.debug(
|
||||||
@@ -149647,7 +149994,7 @@ async function runOpenCode(params) {
|
|||||||
args: params.args,
|
args: params.args,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
env: params.env,
|
env: params.env,
|
||||||
activityTimeout: 0,
|
activityTimeout: 3e5,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
onStdout: async (chunk) => {
|
onStdout: async (chunk) => {
|
||||||
const text = chunk.toString();
|
const text = chunk.toString();
|
||||||
@@ -149704,7 +150051,7 @@ async function runOpenCode(params) {
|
|||||||
} else {
|
} else {
|
||||||
params.todoTracker?.cancel();
|
params.todoTracker?.cancel();
|
||||||
}
|
}
|
||||||
const duration4 = performance6.now() - startTime;
|
const duration4 = performance7.now() - startTime;
|
||||||
log.info(
|
log.info(
|
||||||
`\xBB ${params.label} completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`
|
`\xBB ${params.label} completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`
|
||||||
);
|
);
|
||||||
@@ -149748,7 +150095,7 @@ ${stderrContext}`);
|
|||||||
return { success: true, output: finalOutput || output, usage };
|
return { success: true, output: finalOutput || output, usage };
|
||||||
} catch (error49) {
|
} catch (error49) {
|
||||||
params.todoTracker?.cancel();
|
params.todoTracker?.cancel();
|
||||||
const duration4 = performance6.now() - startTime;
|
const duration4 = performance7.now() - startTime;
|
||||||
const errorMessage = error49 instanceof Error ? error49.message : String(error49);
|
const errorMessage = error49 instanceof Error ? error49.message : String(error49);
|
||||||
const isActivityTimeout = errorMessage.includes("activity timeout");
|
const isActivityTimeout = errorMessage.includes("activity timeout");
|
||||||
const stderrContext = recentStderr.slice(-10).join("\n");
|
const stderrContext = recentStderr.slice(-10).join("\n");
|
||||||
@@ -149781,14 +150128,15 @@ var opentoad = agent({
|
|||||||
});
|
});
|
||||||
const homeEnv = {
|
const homeEnv = {
|
||||||
HOME: ctx.tmpdir,
|
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");
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
||||||
addSkill({
|
addSkill({
|
||||||
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
||||||
skill: "agent-browser",
|
skill: "agent-browser",
|
||||||
env: homeEnv
|
env: homeEnv,
|
||||||
|
agent: "opencode"
|
||||||
});
|
});
|
||||||
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||||
const env2 = {
|
const env2 = {
|
||||||
@@ -149812,10 +150160,35 @@ var opentoad = agent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// agents/index.ts
|
// agents/index.ts
|
||||||
var agents = { opentoad };
|
var agents = { claude, opentoad };
|
||||||
|
|
||||||
// utils/agent.ts
|
// 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;
|
return agents.opentoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149840,7 +150213,7 @@ configure your model at ${settingsUrl}
|
|||||||
|
|
||||||
for full setup instructions, see https://docs.pullfrog.com/keys`;
|
for full setup instructions, see https://docs.pullfrog.com/keys`;
|
||||||
}
|
}
|
||||||
function hasEnvVar(name) {
|
function hasEnvVar2(name) {
|
||||||
const value2 = process.env[name];
|
const value2 = process.env[name];
|
||||||
return typeof value2 === "string" && value2.length > 0;
|
return typeof value2 === "string" && value2.length > 0;
|
||||||
}
|
}
|
||||||
@@ -149848,10 +150221,10 @@ function validateAgentApiKey(params) {
|
|||||||
if (params.model) {
|
if (params.model) {
|
||||||
const requiredVars = getModelEnvVars(params.model);
|
const requiredVars = getModelEnvVars(params.model);
|
||||||
if (requiredVars.length === 0) return;
|
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 }));
|
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) {
|
if (!hasAnyKey) {
|
||||||
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
||||||
}
|
}
|
||||||
@@ -149990,9 +150363,9 @@ ${ctx.error}` : ctx.error;
|
|||||||
|
|
||||||
// utils/gitAuthServer.ts
|
// utils/gitAuthServer.ts
|
||||||
import { randomUUID as randomUUID3 } from "node:crypto";
|
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 { 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 CODE_TTL_MS = 5 * 60 * 1e3;
|
||||||
var TAMPER_WINDOW_MS = 6e4;
|
var TAMPER_WINDOW_MS = 6e4;
|
||||||
function revokeGitHubToken(token) {
|
function revokeGitHubToken(token) {
|
||||||
@@ -150064,7 +150437,7 @@ async function startGitAuthServer(tmpdir2) {
|
|||||||
function writeAskpassScript(code) {
|
function writeAskpassScript(code) {
|
||||||
const scriptId = randomUUID3();
|
const scriptId = randomUUID3();
|
||||||
const scriptName = `askpass-${scriptId}.js`;
|
const scriptName = `askpass-${scriptId}.js`;
|
||||||
const scriptPath = join11(tmpdir2, scriptName);
|
const scriptPath = join12(tmpdir2, scriptName);
|
||||||
const content = [
|
const content = [
|
||||||
`#!/usr/bin/env node`,
|
`#!/usr/bin/env node`,
|
||||||
`var a=process.argv[2]||"";`,
|
`var a=process.argv[2]||"";`,
|
||||||
@@ -150079,7 +150452,7 @@ async function startGitAuthServer(tmpdir2) {
|
|||||||
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
||||||
`})}).on("error",function(){process.exit(1)})}`
|
`})}).on("error",function(){process.exit(1)})}`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
writeFileSync6(scriptPath, content, { mode: 448 });
|
writeFileSync7(scriptPath, content, { mode: 448 });
|
||||||
return scriptPath;
|
return scriptPath;
|
||||||
}
|
}
|
||||||
async function close() {
|
async function close() {
|
||||||
@@ -150337,7 +150710,7 @@ function buildCommonInputs(ctx) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
function assembleFullPrompt(ctx) {
|
function assembleFullPrompt(ctx) {
|
||||||
const learningsSection = ctx.learnings ? `************* REPO INTELLIGENCE *************
|
const learningsSection = ctx.learnings ? `************* LEARNINGS *************
|
||||||
|
|
||||||
${ctx.learnings}` : "";
|
${ctx.learnings}` : "";
|
||||||
const rawFull = `************* RUNTIME CONTEXT *************
|
const rawFull = `************* RUNTIME CONTEXT *************
|
||||||
@@ -150717,7 +151090,8 @@ async function fetchRunContext(params) {
|
|||||||
},
|
},
|
||||||
apiToken: data.apiToken,
|
apiToken: data.apiToken,
|
||||||
oss: data.oss ?? false,
|
oss: data.oss ?? false,
|
||||||
proxyModel: data.proxyModel
|
proxyModel: data.proxyModel,
|
||||||
|
dbSecrets: data.dbSecrets
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@@ -150742,7 +151116,8 @@ async function resolveRunContextData(params) {
|
|||||||
repoSettings: runContext.settings,
|
repoSettings: runContext.settings,
|
||||||
apiToken: runContext.apiToken,
|
apiToken: runContext.apiToken,
|
||||||
oss: runContext.oss,
|
oss: runContext.oss,
|
||||||
proxyModel: runContext.proxyModel
|
proxyModel: runContext.proxyModel,
|
||||||
|
dbSecrets: runContext.dbSecrets
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150750,9 +151125,9 @@ async function resolveRunContextData(params) {
|
|||||||
import { execSync as execSync3 } from "node:child_process";
|
import { execSync as execSync3 } from "node:child_process";
|
||||||
import { mkdtempSync } from "node:fs";
|
import { mkdtempSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join as join12 } from "node:path";
|
import { join as join13 } from "node:path";
|
||||||
function createTempDirectory() {
|
function createTempDirectory() {
|
||||||
const sharedTempDir = mkdtempSync(join12(tmpdir(), "pullfrog-"));
|
const sharedTempDir = mkdtempSync(join13(tmpdir(), "pullfrog-"));
|
||||||
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
||||||
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
||||||
return sharedTempDir;
|
return sharedTempDir;
|
||||||
@@ -151066,6 +151441,16 @@ async function main() {
|
|||||||
const initialOctokit = createOctokit(jobToken);
|
const initialOctokit = createOctokit(jobToken);
|
||||||
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
||||||
timer.checkpoint("runContextData");
|
timer.checkpoint("runContextData");
|
||||||
|
if (runContext.dbSecrets) {
|
||||||
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
||||||
|
if (!process.env[key]) {
|
||||||
|
process.env[key] = value2;
|
||||||
|
core5.setSecret(value2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const count = Object.keys(runContext.dbSecrets).length;
|
||||||
|
if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
|
||||||
|
}
|
||||||
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
||||||
toolState.model = payload.model;
|
toolState.model = payload.model;
|
||||||
if (payload.event.trigger === "pull_request_synchronize") {
|
if (payload.event.trigger === "pull_request_synchronize") {
|
||||||
@@ -151112,7 +151497,7 @@ async function main() {
|
|||||||
const tmpdir2 = createTempDirectory();
|
const tmpdir2 = createTempDirectory();
|
||||||
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir2), true);
|
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir2), true);
|
||||||
setGitAuthServer(gitAuthServer);
|
setGitAuthServer(gitAuthServer);
|
||||||
const agent2 = resolveAgent();
|
const agent2 = resolveAgent({ model: payload.proxyModel ? void 0 : payload.model });
|
||||||
validateAgentApiKey({
|
validateAgentApiKey({
|
||||||
agent: agent2,
|
agent: agent2,
|
||||||
model: payload.proxyModel ?? payload.model,
|
model: payload.proxyModel ?? payload.model,
|
||||||
|
|||||||
@@ -169,6 +169,18 @@ export async function main(): Promise<MainResult> {
|
|||||||
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
||||||
timer.checkpoint("runContextData");
|
timer.checkpoint("runContextData");
|
||||||
|
|
||||||
|
// inject account-level secrets into process.env (YAML secrets take precedence)
|
||||||
|
if (runContext.dbSecrets) {
|
||||||
|
for (const [key, value] of Object.entries(runContext.dbSecrets)) {
|
||||||
|
if (!process.env[key]) {
|
||||||
|
process.env[key] = value;
|
||||||
|
core.setSecret(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const count = Object.keys(runContext.dbSecrets).length;
|
||||||
|
if (count > 0) log.info(`» ${count} db secret(s) loaded`);
|
||||||
|
}
|
||||||
|
|
||||||
// resolve payload to determine shell permission
|
// resolve payload to determine shell permission
|
||||||
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
||||||
toolState.model = payload.model;
|
toolState.model = payload.model;
|
||||||
@@ -237,7 +249,7 @@ export async function main(): Promise<MainResult> {
|
|||||||
await using gitAuthServer = await startGitAuthServer(tmpdir);
|
await using gitAuthServer = await startGitAuthServer(tmpdir);
|
||||||
setGitAuthServer(gitAuthServer);
|
setGitAuthServer(gitAuthServer);
|
||||||
|
|
||||||
const agent = resolveAgent();
|
const agent = resolveAgent({ model: payload.proxyModel ? undefined : payload.model });
|
||||||
|
|
||||||
validateAgentApiKey({
|
validateAgentApiKey({
|
||||||
agent,
|
agent,
|
||||||
|
|||||||
+2
-1
@@ -135,7 +135,8 @@ ${learningsStep(6)}`,
|
|||||||
|
|
||||||
6. Submit:
|
6. Submit:
|
||||||
- **actionable issues found**: call \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, all comments, and an **empty body** — inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary.
|
- **actionable issues found**: call \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, all comments, and an **empty body** — inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary.
|
||||||
- **no actionable issues found**: do NOT submit a review. Call \`${ghPullfrogMcpName}/report_progress\` with a brief note (e.g., "Re-reviewed — no new issues found.").`,
|
- **no actionable issues, but substantive changes or prior fixes confirmed**: post a brief comment (1-3 sentences) via \`${ghPullfrogMcpName}/create_issue_comment\` confirming the review happened and listing which prior review issues were resolved. Substantive = new functionality, behavior changes, architectural changes, or fixes to previously flagged issues.
|
||||||
|
- **no actionable issues, non-substantive changes only** (e.g., trivial formatting, import reordering, comment tweaks with no functional impact): do NOT submit a review. Call \`${ghPullfrogMcpName}/report_progress\` with a brief note (e.g., "Re-reviewed — no new issues found.").`,
|
||||||
|
|
||||||
Plan: `### Checklist
|
Plan: `### Checklist
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -35,7 +35,10 @@ describe("getModelProvider", () => {
|
|||||||
|
|
||||||
describe("getModelEnvVars", () => {
|
describe("getModelEnvVars", () => {
|
||||||
it("returns correct env vars for anthropic", () => {
|
it("returns correct env vars for anthropic", () => {
|
||||||
expect(getModelEnvVars("anthropic/claude-opus")).toEqual(["ANTHROPIC_API_KEY"]);
|
expect(getModelEnvVars("anthropic/claude-opus")).toEqual([
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns correct env vars for google (multiple)", () => {
|
it("returns correct env vars for google (multiple)", () => {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function provider(config: ProviderConfig): ProviderConfig {
|
|||||||
export const providers = {
|
export const providers = {
|
||||||
anthropic: provider({
|
anthropic: provider({
|
||||||
displayName: "Anthropic",
|
displayName: "Anthropic",
|
||||||
envVars: ["ANTHROPIC_API_KEY"],
|
envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||||
models: {
|
models: {
|
||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
@@ -93,7 +93,7 @@ export const providers = {
|
|||||||
}),
|
}),
|
||||||
google: provider({
|
google: provider({
|
||||||
displayName: "Google",
|
displayName: "Google",
|
||||||
envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
|
envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
"gemini-pro": {
|
"gemini-pro": {
|
||||||
displayName: "Gemini Pro",
|
displayName: "Gemini Pro",
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ ${permalinkTip}
|
|||||||
|
|
||||||
7. **SUBMIT** — Determine whether to submit a review:
|
7. **SUBMIT** — Determine whether to submit a review:
|
||||||
- **Issues found**: Submit via ${ghPullfrogMcpName}/create_pull_request_review with \`approved: false\`, the inline comments from step 6, and an **empty body** — inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary (e.g., "Re-reviewed — found 2 issues in the new commits.").
|
- **Issues found**: Submit via ${ghPullfrogMcpName}/create_pull_request_review with \`approved: false\`, the inline comments from step 6, and an **empty body** — inline comments speak for themselves, and a top-level body clutters the PR conversation on every re-review cycle. Then call \`report_progress\` with a 1-sentence summary (e.g., "Re-reviewed — found 2 issues in the new commits.").
|
||||||
- **No issues found**: Do NOT submit a review. Call \`report_progress\` with a brief note (e.g., "Re-reviewed — no new issues found.").
|
- **No issues, but substantive changes or prior fixes confirmed**: Post a brief comment (1-3 sentences) via ${ghPullfrogMcpName}/create_issue_comment confirming the review happened and listing which prior review issues were resolved. Substantive = new functionality, behavior changes, architectural changes, or fixes to previously flagged issues.
|
||||||
|
- **No issues, non-substantive changes only** (e.g., trivial formatting, import reordering, comment tweaks with no functional impact): Do NOT submit a review. Call \`report_progress\` with a brief note (e.g., "Re-reviewed — no new issues found.").
|
||||||
|
|
||||||
${permalinkTip}
|
${permalinkTip}
|
||||||
`,
|
`,
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.184",
|
"version": "0.0.186",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"turndown": "^7.2.0"
|
"turndown": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@anthropic-ai/claude-code": "2.1.85",
|
||||||
"agent-browser": "0.21.0",
|
"agent-browser": "0.21.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@types/node": "^24.7.2",
|
"@types/node": "^24.7.2",
|
||||||
|
|||||||
Generated
+171
@@ -66,6 +66,9 @@ importers:
|
|||||||
specifier: ^7.2.0
|
specifier: ^7.2.0
|
||||||
version: 7.2.2
|
version: 7.2.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@anthropic-ai/claude-code':
|
||||||
|
specifier: 2.1.85
|
||||||
|
version: 2.1.85
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.26.0
|
specifier: ^1.26.0
|
||||||
version: 1.26.0(zod@4.3.6)
|
version: 1.26.0(zod@4.3.6)
|
||||||
@@ -117,6 +120,11 @@ packages:
|
|||||||
'@actions/io@1.1.3':
|
'@actions/io@1.1.3':
|
||||||
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
||||||
|
|
||||||
|
'@anthropic-ai/claude-code@2.1.85':
|
||||||
|
resolution: {integrity: sha512-3/q3xTpk9EnBfQ/XsHGkOZniOgQx4sqD95CDKw1mvN1Qw5+9IZTp6ILdds02d7vOM6YuLL0G0zhqsMSAFVse4w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@ark/fs@0.56.0':
|
'@ark/fs@0.56.0':
|
||||||
resolution: {integrity: sha512-zY/wDDhcvmt6/upQwZM766PAnvIzdEMcgydUGd9pqY9FMGNo9I9uE4RYAfms9AeUUtbZJu2h2Ua0tvFsO5XF4Q==}
|
resolution: {integrity: sha512-zY/wDDhcvmt6/upQwZM766PAnvIzdEMcgydUGd9pqY9FMGNo9I9uE4RYAfms9AeUUtbZJu2h2Ua0tvFsO5XF4Q==}
|
||||||
|
|
||||||
@@ -457,6 +465,95 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
hono: ^4
|
hono: ^4
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
|
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5':
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
@@ -1827,6 +1924,18 @@ snapshots:
|
|||||||
|
|
||||||
'@actions/io@1.1.3': {}
|
'@actions/io@1.1.3': {}
|
||||||
|
|
||||||
|
'@anthropic-ai/claude-code@2.1.85':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.34.5
|
||||||
|
'@img/sharp-darwin-x64': 0.34.5
|
||||||
|
'@img/sharp-linux-arm': 0.34.5
|
||||||
|
'@img/sharp-linux-arm64': 0.34.5
|
||||||
|
'@img/sharp-linux-x64': 0.34.5
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||||
|
'@img/sharp-win32-arm64': 0.34.5
|
||||||
|
'@img/sharp-win32-x64': 0.34.5
|
||||||
|
|
||||||
'@ark/fs@0.56.0': {}
|
'@ark/fs@0.56.0': {}
|
||||||
|
|
||||||
'@ark/schema@0.56.0':
|
'@ark/schema@0.56.0':
|
||||||
@@ -2003,6 +2112,68 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.0
|
hono: 4.12.0
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
'@mixmark-io/domino@2.2.0': {}
|
'@mixmark-io/domino@2.2.0': {}
|
||||||
|
|||||||
@@ -37516,7 +37516,7 @@ function provider(config) {
|
|||||||
var providers = {
|
var providers = {
|
||||||
anthropic: provider({
|
anthropic: provider({
|
||||||
displayName: "Anthropic",
|
displayName: "Anthropic",
|
||||||
envVars: ["ANTHROPIC_API_KEY"],
|
envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
||||||
models: {
|
models: {
|
||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
@@ -37559,7 +37559,7 @@ var providers = {
|
|||||||
}),
|
}),
|
||||||
google: provider({
|
google: provider({
|
||||||
displayName: "Google",
|
displayName: "Google",
|
||||||
envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
|
envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
"gemini-pro": {
|
"gemini-pro": {
|
||||||
displayName: "Gemini Pro",
|
displayName: "Gemini Pro",
|
||||||
@@ -41557,7 +41557,7 @@ var core3 = __toESM(require_core(), 1);
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.184",
|
version: "0.0.186",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -41603,6 +41603,7 @@ var package_default = {
|
|||||||
turndown: "^7.2.0"
|
turndown: "^7.2.0"
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
|
"@anthropic-ai/claude-code": "2.1.85",
|
||||||
"agent-browser": "0.21.0",
|
"agent-browser": "0.21.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@types/node": "^24.7.2",
|
"@types/node": "^24.7.2",
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ exports[`latest model per provider snapshot > matches snapshot 1`] = `
|
|||||||
"releaseDate": "2026-03-17",
|
"releaseDate": "2026-03-17",
|
||||||
},
|
},
|
||||||
"opencode": {
|
"opencode": {
|
||||||
"modelId": "mimo-v2-pro-free",
|
"modelId": "qwen3.6-plus-free",
|
||||||
"releaseDate": "2026-03-18",
|
"releaseDate": "2026-03-30",
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
"modelId": "xiaomi/mimo-v2-pro",
|
"modelId": "qwen/qwen3.6-plus-preview:free",
|
||||||
"releaseDate": "2026-03-18",
|
"releaseDate": "2026-03-30",
|
||||||
},
|
},
|
||||||
"xai": {
|
"xai": {
|
||||||
"modelId": "grok-4.20-multi-agent-0309",
|
"modelId": "grok-4.20-multi-agent-0309",
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ describe("ci workflow consistency", () => {
|
|||||||
|
|
||||||
it("changed-agents.sh treats legacy agent files as non-agent changes", () => {
|
it("changed-agents.sh treats legacy agent files as non-agent changes", () => {
|
||||||
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
|
const output = execFileSync("bash", [join(__dirname, "changed-agents.sh")], {
|
||||||
input: JSON.stringify(["action/agents/claude.ts", "action/agents/gemini.ts"]),
|
input: JSON.stringify(["action/agents/codex.ts", "action/agents/gemini.ts"]),
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
});
|
});
|
||||||
expect(JSON.parse(output)).toEqual(["opentoad"]);
|
expect(JSON.parse(output)).toEqual(["opentoad"]);
|
||||||
|
|||||||
+36
-1
@@ -1,6 +1,41 @@
|
|||||||
import type { Agent } from "../agents/index.ts";
|
import type { Agent } from "../agents/index.ts";
|
||||||
import { agents } from "../agents/index.ts";
|
import { agents } from "../agents/index.ts";
|
||||||
|
import { getModelProvider } from "../models.ts";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
|
|
||||||
export function resolveAgent(): Agent {
|
function hasEnvVar(name: string): boolean {
|
||||||
|
const val = process.env[name];
|
||||||
|
return typeof val === "string" && val.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasClaudeCodeAuth(): boolean {
|
||||||
|
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAgent(ctx?: { model?: string | undefined }): Agent {
|
||||||
|
// 1. explicit env var override (escape hatch)
|
||||||
|
const envAgent = process.env.PULLFROG_AGENT?.trim();
|
||||||
|
if (envAgent) {
|
||||||
|
if (envAgent in agents) {
|
||||||
|
log.info(`» agent: ${envAgent} (override via PULLFROG_AGENT)`);
|
||||||
|
return agents[envAgent as keyof typeof agents];
|
||||||
|
}
|
||||||
|
log.warning(`» unknown PULLFROG_AGENT="${envAgent}" — falling through to auto-select`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. if model is Anthropic and Claude Code credentials are available, use Claude Code
|
||||||
|
if (ctx?.model) {
|
||||||
|
try {
|
||||||
|
const provider = getModelProvider(ctx.model);
|
||||||
|
if (provider === "anthropic" && hasClaudeCodeAuth()) {
|
||||||
|
log.info(`» agent: claude (auto-selected for ${ctx.model})`);
|
||||||
|
return agents.claude;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// invalid model slug format — fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. default: OpenCode (universal, supports all providers)
|
||||||
return agents.opentoad;
|
return agents.opentoad;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const savedEnv = { ...process.env };
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// strip all known provider keys so tests start clean
|
// strip all known provider keys so tests start clean
|
||||||
for (const key of Object.keys(process.env)) {
|
for (const key of Object.keys(process.env)) {
|
||||||
if (key.endsWith("_API_KEY")) delete process.env[key];
|
if (key.endsWith("_API_KEY") || key === "CLAUDE_CODE_OAUTH_TOKEN") delete process.env[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ const testEnvAllowList = new Set([
|
|||||||
"GITHUB_PRIVATE_KEY",
|
"GITHUB_PRIVATE_KEY",
|
||||||
"OPENAI_API_KEY",
|
"OPENAI_API_KEY",
|
||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
"GEMINI_API_KEY",
|
"GEMINI_API_KEY",
|
||||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||||
"PULLFROG_MODEL",
|
"PULLFROG_MODEL",
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ interface AssembleFullPromptInput {
|
|||||||
|
|
||||||
function assembleFullPrompt(ctx: AssembleFullPromptInput): string {
|
function assembleFullPrompt(ctx: AssembleFullPromptInput): string {
|
||||||
const learningsSection = ctx.learnings
|
const learningsSection = ctx.learnings
|
||||||
? `************* REPO INTELLIGENCE *************\n\n${ctx.learnings}`
|
? `************* LEARNINGS *************\n\n${ctx.learnings}`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const rawFull = `************* RUNTIME CONTEXT *************
|
const rawFull = `************* RUNTIME CONTEXT *************
|
||||||
|
|||||||
+9
-25
@@ -339,40 +339,24 @@ export function formatIndentedField(label: string, content: string): string {
|
|||||||
export function formatUsageSummary(entries: AgentUsage[]): string {
|
export function formatUsageSummary(entries: AgentUsage[]): string {
|
||||||
if (entries.length === 0) return "";
|
if (entries.length === 0) return "";
|
||||||
|
|
||||||
const hasCost = entries.some((e) => e.costUsd !== undefined);
|
const header = "| Agent | Input | Output | Cache Read | Cache Write |";
|
||||||
|
const separatorRow = "| --- | ---: | ---: | ---: | ---: |";
|
||||||
const header = hasCost
|
|
||||||
? "| Agent | Input | Output | Cache Read | Cache Write | Cost |"
|
|
||||||
: "| Agent | Input | Output | Cache Read | Cache Write |";
|
|
||||||
|
|
||||||
const fmt = (n: number) => n.toLocaleString("en-US");
|
const fmt = (n: number) => n.toLocaleString("en-US");
|
||||||
|
|
||||||
const separatorRow = hasCost
|
const rows = entries.map(
|
||||||
? "| --- | ---: | ---: | ---: | ---: | ---: |"
|
(e) =>
|
||||||
: "| --- | ---: | ---: | ---: | ---: |";
|
`| ${e.agent} | ${fmt(e.inputTokens)} | ${fmt(e.outputTokens)} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} |`
|
||||||
|
);
|
||||||
|
|
||||||
const rows = entries.map((e) => {
|
|
||||||
const base = `| ${e.agent} | ${fmt(e.inputTokens)} | ${fmt(e.outputTokens)} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} |`;
|
|
||||||
return hasCost
|
|
||||||
? `${base} ${e.costUsd !== undefined ? `$${e.costUsd.toFixed(4)}` : "-"} |`
|
|
||||||
: base;
|
|
||||||
});
|
|
||||||
|
|
||||||
// totals row (only useful when there are multiple entries)
|
|
||||||
const totalsRows: string[] = [];
|
const totalsRows: string[] = [];
|
||||||
if (entries.length > 1) {
|
if (entries.length > 1) {
|
||||||
const totalInput = entries.reduce((sum, e) => sum + e.inputTokens, 0);
|
const totalInput = entries.reduce((sum, e) => sum + e.inputTokens, 0);
|
||||||
const totalOutput = entries.reduce((sum, e) => sum + e.outputTokens, 0);
|
const totalOutput = entries.reduce((sum, e) => sum + e.outputTokens, 0);
|
||||||
const totalCacheRead = entries.reduce((sum, e) => sum + (e.cacheReadTokens ?? 0), 0);
|
const totalCacheRead = entries.reduce((sum, e) => sum + (e.cacheReadTokens ?? 0), 0);
|
||||||
const totalCacheWrite = entries.reduce((sum, e) => sum + (e.cacheWriteTokens ?? 0), 0);
|
const totalCacheWrite = entries.reduce((sum, e) => sum + (e.cacheWriteTokens ?? 0), 0);
|
||||||
const totalBase = `| **Total** | **${fmt(totalInput)}** | **${fmt(totalOutput)}** | **${fmt(totalCacheRead)}** | **${fmt(totalCacheWrite)}** |`;
|
totalsRows.push(
|
||||||
|
`| **Total** | **${fmt(totalInput)}** | **${fmt(totalOutput)}** | **${fmt(totalCacheRead)}** | **${fmt(totalCacheWrite)}** |`
|
||||||
if (hasCost) {
|
);
|
||||||
const totalCost = entries.reduce((sum, e) => sum + (e.costUsd ?? 0), 0);
|
|
||||||
totalsRows.push(`${totalBase} **$${totalCost.toFixed(4)}** |`);
|
|
||||||
} else {
|
|
||||||
totalsRows.push(totalBase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const 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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function detectProviderError(text: string): string | null {
|
||||||
|
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
||||||
|
if (text.includes(entry.pattern)) return entry.label;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ export interface RunContext {
|
|||||||
apiToken: string;
|
apiToken: string;
|
||||||
oss: boolean;
|
oss: boolean;
|
||||||
proxyModel?: string | undefined;
|
proxyModel?: string | undefined;
|
||||||
|
dbSecrets?: Record<string, string> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSettings: RepoSettings = {
|
const defaultSettings: RepoSettings = {
|
||||||
@@ -82,6 +83,7 @@ export async function fetchRunContext(params: {
|
|||||||
apiToken: string;
|
apiToken: string;
|
||||||
oss?: boolean;
|
oss?: boolean;
|
||||||
proxyModel?: string;
|
proxyModel?: string;
|
||||||
|
dbSecrets?: Record<string, string>;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
@@ -100,6 +102,7 @@ export async function fetchRunContext(params: {
|
|||||||
apiToken: data.apiToken,
|
apiToken: data.apiToken,
|
||||||
oss: data.oss ?? false,
|
oss: data.oss ?? false,
|
||||||
proxyModel: data.proxyModel,
|
proxyModel: data.proxyModel,
|
||||||
|
dbSecrets: data.dbSecrets,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface RunContextData {
|
|||||||
apiToken: string;
|
apiToken: string;
|
||||||
oss: boolean;
|
oss: boolean;
|
||||||
proxyModel?: string | undefined;
|
proxyModel?: string | undefined;
|
||||||
|
dbSecrets?: Record<string, string> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolveRunContextDataParams {
|
interface ResolveRunContextDataParams {
|
||||||
@@ -46,5 +47,6 @@ export async function resolveRunContextData(
|
|||||||
apiToken: runContext.apiToken,
|
apiToken: runContext.apiToken,
|
||||||
oss: runContext.oss,
|
oss: runContext.oss,
|
||||||
proxyModel: runContext.proxyModel,
|
proxyModel: runContext.proxyModel,
|
||||||
|
dbSecrets: runContext.dbSecrets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
|
|
||||||
|
export function addSkill(params: {
|
||||||
|
ref: string;
|
||||||
|
skill: string;
|
||||||
|
env: Record<string, string>;
|
||||||
|
agent: string;
|
||||||
|
}): void {
|
||||||
|
const result = spawnSync(
|
||||||
|
"npx",
|
||||||
|
["skills", "add", params.ref, "--skill", params.skill, "-g", "-a", params.agent, "-y"],
|
||||||
|
{
|
||||||
|
env: { ...process.env, ...params.env },
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 30_000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result.status === 0) {
|
||||||
|
log.info(`installed ${params.skill} skill (${params.agent})`);
|
||||||
|
} else {
|
||||||
|
log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-16
@@ -76,7 +76,8 @@ export interface SpawnOptions {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
input?: string;
|
input?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
// activity timeout: kill process if no stdout/stderr for this many ms (default: 30s, 0 to disable)
|
// activity timeout: kill process if no stdout for this many ms (default: 30s, 0 to disable).
|
||||||
|
// only stdout resets the timer — stderr (e.g. provider error retries) does not count as progress.
|
||||||
activityTimeout?: number;
|
activityTimeout?: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
stdio?: ("pipe" | "ignore" | "inherit")[];
|
stdio?: ("pipe" | "ignore" | "inherit")[];
|
||||||
@@ -95,7 +96,6 @@ export interface SpawnResult {
|
|||||||
* Spawn a subprocess with streaming callbacks and buffered results
|
* Spawn a subprocess with streaming callbacks and buffered results
|
||||||
*/
|
*/
|
||||||
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||||
const { cmd, args, env, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
|
||||||
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||||
|
|
||||||
installSignalHandler();
|
installSignalHandler();
|
||||||
@@ -106,13 +106,13 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// security: caller must provide complete env object, not merged with process.env
|
// security: caller must provide complete env object, not merged with process.env
|
||||||
const child = nodeSpawn(cmd, args, {
|
const child = nodeSpawn(options.cmd, options.args, {
|
||||||
env: env || {
|
env: options.env || {
|
||||||
PATH: process.env.PATH || "",
|
PATH: process.env.PATH || "",
|
||||||
HOME: process.env.HOME || "",
|
HOME: process.env.HOME || "",
|
||||||
},
|
},
|
||||||
stdio: stdio || ["pipe", "pipe", "pipe"],
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
||||||
cwd: cwd || process.cwd(),
|
cwd: options.cwd || process.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// track child for cleanup on Ctrl+C
|
// track child for cleanup on Ctrl+C
|
||||||
@@ -125,7 +125,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
let lastActivityTime = performance.now();
|
let lastActivityTime = performance.now();
|
||||||
|
|
||||||
// overall timeout
|
// overall timeout
|
||||||
if (timeout) {
|
if (options.timeout) {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
isTimedOut = true;
|
isTimedOut = true;
|
||||||
child.kill("SIGTERM");
|
child.kill("SIGTERM");
|
||||||
@@ -135,12 +135,14 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}, timeout);
|
}, options.timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// activity timeout: kill if no output for too long
|
// activity timeout: kill if no output for too long
|
||||||
if (activityTimeoutMs > 0) {
|
if (activityTimeoutMs > 0) {
|
||||||
log.debug(`spawn activity timer: pid=${child.pid} cmd=${cmd} timeout=${activityTimeoutMs}ms`);
|
log.debug(
|
||||||
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
||||||
|
);
|
||||||
activityCheckIntervalId = setInterval(() => {
|
activityCheckIntervalId = setInterval(() => {
|
||||||
const idleMs = performance.now() - lastActivityTime;
|
const idleMs = performance.now() - lastActivityTime;
|
||||||
log.debug(
|
log.debug(
|
||||||
@@ -149,7 +151,9 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
if (idleMs > activityTimeoutMs) {
|
if (idleMs > activityTimeoutMs) {
|
||||||
isActivityTimedOut = true;
|
isActivityTimedOut = true;
|
||||||
const idleSec = Math.round(idleMs / 1000);
|
const idleSec = Math.round(idleMs / 1000);
|
||||||
log.info(`no output for ${idleSec}s from pid=${child.pid} (${cmd}), killing process`);
|
log.info(
|
||||||
|
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
||||||
|
);
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
clearInterval(activityCheckIntervalId);
|
clearInterval(activityCheckIntervalId);
|
||||||
}
|
}
|
||||||
@@ -165,16 +169,15 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
updateActivity();
|
updateActivity();
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
stdoutBuffer += chunk;
|
stdoutBuffer += chunk;
|
||||||
onStdout?.(chunk);
|
options.onStdout?.(chunk);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.stderr) {
|
if (child.stderr) {
|
||||||
child.stderr.on("data", (data: Buffer) => {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
updateActivity();
|
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
stderrBuffer += chunk;
|
stderrBuffer += chunk;
|
||||||
onStderr?.(chunk);
|
options.onStderr?.(chunk);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +189,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
||||||
|
|
||||||
if (isTimedOut) {
|
if (isTimedOut) {
|
||||||
reject(new Error(`process timed out after ${timeout}ms`));
|
reject(new Error(`process timed out after ${options.timeout}ms`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +225,8 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input && child.stdin && stdio?.[0] !== "ignore") {
|
if (options.input && child.stdin && options.stdio?.[0] !== "ignore") {
|
||||||
child.stdin.write(input);
|
child.stdin.write(options.input);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user