opencode initial run
This commit is contained in:
@@ -3,6 +3,7 @@ import { claude } from "./claude.ts";
|
|||||||
import { codex } from "./codex.ts";
|
import { codex } from "./codex.ts";
|
||||||
import { cursor } from "./cursor.ts";
|
import { cursor } from "./cursor.ts";
|
||||||
import { gemini } from "./gemini.ts";
|
import { gemini } from "./gemini.ts";
|
||||||
|
import { opencode } from "./opencode.ts";
|
||||||
import type { Agent } from "./shared.ts";
|
import type { Agent } from "./shared.ts";
|
||||||
|
|
||||||
export const agents = {
|
export const agents = {
|
||||||
@@ -10,4 +11,5 @@ export const agents = {
|
|||||||
codex,
|
codex,
|
||||||
cursor,
|
cursor,
|
||||||
gemini,
|
gemini,
|
||||||
|
opencode,
|
||||||
} satisfies Record<AgentName, Agent>;
|
} satisfies Record<AgentName, Agent>;
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { log } from "../utils/cli.ts";
|
||||||
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
|
import { addInstructions } from "./instructions.ts";
|
||||||
|
import {
|
||||||
|
agent,
|
||||||
|
type ConfigureMcpServersParams,
|
||||||
|
createAgentEnv,
|
||||||
|
installFromNpmTarball,
|
||||||
|
setupProcessAgentEnv,
|
||||||
|
} from "./shared.ts";
|
||||||
|
|
||||||
|
// opencode cli event types inferred from json output format
|
||||||
|
interface OpenCodeInitEvent {
|
||||||
|
type: "init";
|
||||||
|
timestamp?: string;
|
||||||
|
session_id?: string;
|
||||||
|
model?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeMessageEvent {
|
||||||
|
type: "message";
|
||||||
|
timestamp?: string;
|
||||||
|
role?: "user" | "assistant";
|
||||||
|
content?: string;
|
||||||
|
delta?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeTextEvent {
|
||||||
|
type: "text";
|
||||||
|
timestamp?: string;
|
||||||
|
sessionID?: string;
|
||||||
|
part?: {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
text?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeStepStartEvent {
|
||||||
|
type: "step_start";
|
||||||
|
timestamp?: string;
|
||||||
|
sessionID?: string;
|
||||||
|
part?: {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeStepFinishEvent {
|
||||||
|
type: "step_finish";
|
||||||
|
timestamp?: string;
|
||||||
|
sessionID?: string;
|
||||||
|
part?: {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
reason?: string;
|
||||||
|
cost?: number;
|
||||||
|
tokens?: {
|
||||||
|
input?: number;
|
||||||
|
output?: number;
|
||||||
|
reasoning?: number;
|
||||||
|
cache?: {
|
||||||
|
read?: number;
|
||||||
|
write?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeToolUseEvent {
|
||||||
|
type: "tool_use";
|
||||||
|
timestamp?: string;
|
||||||
|
tool_name?: string;
|
||||||
|
tool_id?: string;
|
||||||
|
parameters?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeToolResultEvent {
|
||||||
|
type: "tool_result";
|
||||||
|
timestamp?: string;
|
||||||
|
tool_id?: string;
|
||||||
|
status?: "success" | "error";
|
||||||
|
output?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeResultEvent {
|
||||||
|
type: "result";
|
||||||
|
timestamp?: string;
|
||||||
|
status?: "success" | "error";
|
||||||
|
stats?: {
|
||||||
|
total_tokens?: number;
|
||||||
|
input_tokens?: number;
|
||||||
|
output_tokens?: number;
|
||||||
|
duration_ms?: number;
|
||||||
|
tool_calls?: number;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenCodeEvent =
|
||||||
|
| OpenCodeInitEvent
|
||||||
|
| OpenCodeMessageEvent
|
||||||
|
| OpenCodeTextEvent
|
||||||
|
| OpenCodeStepStartEvent
|
||||||
|
| OpenCodeStepFinishEvent
|
||||||
|
| OpenCodeToolUseEvent
|
||||||
|
| OpenCodeToolResultEvent
|
||||||
|
| OpenCodeResultEvent;
|
||||||
|
|
||||||
|
let finalOutput = "";
|
||||||
|
let accumulatedTokens: { input: number; output: number } = { input: 0, output: 0 };
|
||||||
|
let tokensLogged = false;
|
||||||
|
|
||||||
|
const messageHandlers = {
|
||||||
|
init: (_event: OpenCodeInitEvent) => {
|
||||||
|
// initialization event - reset state
|
||||||
|
finalOutput = "";
|
||||||
|
accumulatedTokens = { input: 0, output: 0 };
|
||||||
|
tokensLogged = false;
|
||||||
|
},
|
||||||
|
message: (event: OpenCodeMessageEvent) => {
|
||||||
|
// update finalOutput but don't log here - text handler will log
|
||||||
|
if (event.role === "assistant" && event.content?.trim() && !event.delta) {
|
||||||
|
const message = event.content.trim();
|
||||||
|
if (message) {
|
||||||
|
finalOutput = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: (event: OpenCodeTextEvent) => {
|
||||||
|
// log from text events only to avoid duplicates
|
||||||
|
if (event.part?.text?.trim()) {
|
||||||
|
const message = event.part.text.trim();
|
||||||
|
log.box(message, { title: "OpenCode" });
|
||||||
|
finalOutput = message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
step_start: (_event: OpenCodeStepStartEvent) => {
|
||||||
|
// step start - no logging needed
|
||||||
|
},
|
||||||
|
step_finish: async (event: OpenCodeStepFinishEvent) => {
|
||||||
|
// accumulate tokens from step_finish events (they come here, not in result)
|
||||||
|
const eventTokens = event.part?.tokens;
|
||||||
|
if (eventTokens) {
|
||||||
|
const inputTokens = eventTokens.input || 0;
|
||||||
|
const outputTokens = eventTokens.output || 0;
|
||||||
|
|
||||||
|
// accumulate tokens (don't log yet - wait for result event)
|
||||||
|
accumulatedTokens.input += inputTokens;
|
||||||
|
accumulatedTokens.output += outputTokens;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tool_use: (event: OpenCodeToolUseEvent) => {
|
||||||
|
if (event.tool_name) {
|
||||||
|
log.toolCall({
|
||||||
|
toolName: event.tool_name,
|
||||||
|
input: event.parameters || {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tool_result: (event: OpenCodeToolResultEvent) => {
|
||||||
|
if (event.status === "error") {
|
||||||
|
const errorMsg =
|
||||||
|
typeof event.output === "string" ? event.output : JSON.stringify(event.output);
|
||||||
|
log.warning(`Tool call failed: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result: async (event: OpenCodeResultEvent) => {
|
||||||
|
if (event.status === "error") {
|
||||||
|
log.error(`OpenCode CLI failed: ${JSON.stringify(event)}`);
|
||||||
|
} else {
|
||||||
|
// log tokens once at the end (use stats from result if available, otherwise use accumulated from step_finish)
|
||||||
|
const inputTokens = event.stats?.input_tokens || accumulatedTokens.input || 0;
|
||||||
|
const outputTokens = event.stats?.output_tokens || accumulatedTokens.output || 0;
|
||||||
|
const totalTokens = event.stats?.total_tokens || inputTokens + outputTokens;
|
||||||
|
|
||||||
|
if ((inputTokens > 0 || outputTokens > 0) && !tokensLogged) {
|
||||||
|
await log.summaryTable([
|
||||||
|
[
|
||||||
|
{ data: "Input Tokens", header: true },
|
||||||
|
{ data: "Output Tokens", header: true },
|
||||||
|
{ data: "Total Tokens", header: true },
|
||||||
|
],
|
||||||
|
[String(inputTokens), String(outputTokens), String(totalTokens)],
|
||||||
|
]);
|
||||||
|
tokensLogged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const opencode = agent({
|
||||||
|
name: "opencode",
|
||||||
|
install: async () => {
|
||||||
|
return await installFromNpmTarball({
|
||||||
|
packageName: "opencode-ai",
|
||||||
|
version: "latest",
|
||||||
|
executablePath: "bin/opencode",
|
||||||
|
installDependencies: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
run: async ({ payload, apiKey, mcpServers, cliPath }) => {
|
||||||
|
// 1. configure home/config directory
|
||||||
|
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
const configDir = join(tempHome, ".config", "opencode");
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
|
||||||
|
// 2. initialize MCP servers and sandbox
|
||||||
|
configureOpenCodeMcpServers({ mcpServers });
|
||||||
|
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("anthropic_api_key is required for opencode agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. prepare prompt and args
|
||||||
|
const prompt = addInstructions(payload);
|
||||||
|
const args = ["run", "--format", "json", "-m", "anthropic/claude-sonnet-4-20250514", prompt];
|
||||||
|
|
||||||
|
if (payload.sandbox) {
|
||||||
|
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. set up environment
|
||||||
|
const packageDir = join(cliPath, "..", "..");
|
||||||
|
setupProcessAgentEnv({ ANTHROPIC_API_KEY: apiKey, HOME: tempHome });
|
||||||
|
const env = createAgentEnv({ ANTHROPIC_API_KEY: apiKey, HOME: tempHome });
|
||||||
|
|
||||||
|
// 5. spawn and stream JSON output
|
||||||
|
let output = "";
|
||||||
|
const result = await spawn({
|
||||||
|
cmd: cliPath,
|
||||||
|
args,
|
||||||
|
cwd: packageDir,
|
||||||
|
env,
|
||||||
|
timeout: 300000,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
onStdout: async (chunk) => {
|
||||||
|
const text = chunk.toString();
|
||||||
|
output += text;
|
||||||
|
|
||||||
|
// parse each line as JSON (opencode outputs one JSON object per line)
|
||||||
|
const lines = text.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(trimmed) as OpenCodeEvent;
|
||||||
|
const handler = messageHandlers[event.type as keyof typeof messageHandlers];
|
||||||
|
if (handler) {
|
||||||
|
await handler(event as never);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// non-JSON lines are ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStderr: (chunk) => {
|
||||||
|
const trimmed = chunk.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
log.warning(trimmed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. log tokens if they weren't logged yet (fallback if result event wasn't emitted)
|
||||||
|
if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0)) {
|
||||||
|
const totalTokens = accumulatedTokens.input + accumulatedTokens.output;
|
||||||
|
await log.summaryTable([
|
||||||
|
[
|
||||||
|
{ data: "Input Tokens", header: true },
|
||||||
|
{ data: "Output Tokens", header: true },
|
||||||
|
{ data: "Total Tokens", header: true },
|
||||||
|
],
|
||||||
|
[String(accumulatedTokens.input), String(accumulatedTokens.output), String(totalTokens)],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. return result
|
||||||
|
return {
|
||||||
|
success: result.exitCode === 0,
|
||||||
|
output: finalOutput || output,
|
||||||
|
error: result.exitCode !== 0 ? result.stderr : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure MCP servers for OpenCode using opencode.json config file.
|
||||||
|
* OpenCode uses opencode.json with mcp section supporting remote servers with type: "remote" and url.
|
||||||
|
*/
|
||||||
|
function configureOpenCodeMcpServers({
|
||||||
|
mcpServers,
|
||||||
|
}: {
|
||||||
|
mcpServers: ConfigureMcpServersParams["mcpServers"];
|
||||||
|
}): void {
|
||||||
|
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
const configDir = join(tempHome, ".config", "opencode");
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
const configPath = join(configDir, "opencode.json");
|
||||||
|
|
||||||
|
// convert to opencode's expected format
|
||||||
|
const opencodeMcpServers: Record<string, { type: "remote"; url: string; enabled?: boolean }> = {};
|
||||||
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||||
|
if (serverConfig.type !== "http") {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported MCP server type for OpenCode: ${(serverConfig as any).type || "unknown"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
opencodeMcpServers[serverName] = {
|
||||||
|
type: "remote",
|
||||||
|
url: serverConfig.url,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// read existing config if it exists, or create new one
|
||||||
|
let config: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
if (existsSync(configPath)) {
|
||||||
|
const existingConfig = readFileSync(configPath, "utf-8");
|
||||||
|
config = JSON.parse(existingConfig);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// config doesn't exist yet or is invalid, start fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
config.mcp = opencodeMcpServers;
|
||||||
|
|
||||||
|
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||||
|
log.info(`MCP config written to ${configPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure OpenCode sandbox mode via opencode.json.
|
||||||
|
* When sandbox is enabled, restricts tools to read-only operations.
|
||||||
|
*/
|
||||||
|
function configureOpenCodeSandbox({ sandbox }: { sandbox: boolean }): void {
|
||||||
|
const tempHome = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
const configDir = join(tempHome, ".config", "opencode");
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
const configPath = join(configDir, "opencode.json");
|
||||||
|
|
||||||
|
// read existing config if it exists, or create new one
|
||||||
|
let config: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
if (existsSync(configPath)) {
|
||||||
|
const existingConfig = readFileSync(configPath, "utf-8");
|
||||||
|
config = JSON.parse(existingConfig);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// config doesn't exist yet or is invalid, start fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sandbox) {
|
||||||
|
// sandbox mode: disable write, bash, and webfetch tools
|
||||||
|
config.tools = {
|
||||||
|
write: false,
|
||||||
|
bash: false,
|
||||||
|
webfetch: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// normal mode: enable all tools (or don't set tools config to use defaults)
|
||||||
|
config.tools = {
|
||||||
|
write: true,
|
||||||
|
bash: true,
|
||||||
|
webfetch: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// preserve MCP config if it was already set by configureOpenCodeMcpServers
|
||||||
|
// (this function is called after configureOpenCodeMcpServers, so MCP config should already exist)
|
||||||
|
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||||
|
log.info(`OpenCode config written to ${configPath} (sandbox: ${sandbox})`);
|
||||||
|
}
|
||||||
@@ -96,6 +96,17 @@ export interface InstallFromGithubParams {
|
|||||||
githubInstallationToken?: string;
|
githubInstallationToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for installing from GitHub releases tarball
|
||||||
|
*/
|
||||||
|
export interface InstallFromGithubTarballParams {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
assetNamePattern: string;
|
||||||
|
executablePath: string;
|
||||||
|
githubInstallationToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NPM registry response data structure
|
* NPM registry response data structure
|
||||||
*/
|
*/
|
||||||
@@ -318,6 +329,92 @@ export async function installFromGithub({
|
|||||||
return cliPath;
|
return cliPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a CLI tool from a GitHub release tarball
|
||||||
|
* Downloads the tar.gz from GitHub releases, extracts it, and returns the path to the CLI executable
|
||||||
|
* The temp directory will be cleaned up by the OS automatically
|
||||||
|
*/
|
||||||
|
export async function installFromGithubTarball({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
assetNamePattern,
|
||||||
|
executablePath,
|
||||||
|
githubInstallationToken,
|
||||||
|
}: InstallFromGithubTarballParams): Promise<string> {
|
||||||
|
log.info(`📦 Installing ${owner}/${repo} from GitHub releases...`);
|
||||||
|
|
||||||
|
// determine platform-specific asset name
|
||||||
|
const os = process.platform === "darwin" ? "darwin" : "linux";
|
||||||
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
||||||
|
const assetName = assetNamePattern.replace("{os}", os).replace("{arch}", arch);
|
||||||
|
|
||||||
|
// fetch release from GitHub API (latest)
|
||||||
|
const releaseUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||||
|
log.info(`Fetching release from ${releaseUrl}...`);
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (githubInstallationToken) {
|
||||||
|
headers.Authorization = `Bearer ${githubInstallationToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseResponse = await fetchWithRetry(releaseUrl, headers, "Failed to fetch release");
|
||||||
|
|
||||||
|
const releaseData = (await releaseResponse.json()) as {
|
||||||
|
tag_name: string;
|
||||||
|
assets: Array<{
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info(`Found release: ${releaseData.tag_name}`);
|
||||||
|
|
||||||
|
const asset = releaseData.assets.find((a) => a.name === assetName);
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(`Asset '${assetName}' not found in release ${releaseData.tag_name}`);
|
||||||
|
}
|
||||||
|
const assetUrl = asset.browser_download_url;
|
||||||
|
|
||||||
|
log.info(`Downloading asset from ${assetUrl}...`);
|
||||||
|
|
||||||
|
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
const tarballPath = join(tempDir, assetName);
|
||||||
|
|
||||||
|
// download the asset
|
||||||
|
const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
|
||||||
|
|
||||||
|
if (!assetResponse.body) throw new Error("Response body is null");
|
||||||
|
const fileStream = createWriteStream(tarballPath);
|
||||||
|
await pipeline(assetResponse.body, fileStream);
|
||||||
|
log.info(`Downloaded tarball to ${tarballPath}`);
|
||||||
|
|
||||||
|
// extract tar.gz
|
||||||
|
log.info(`Extracting tarball...`);
|
||||||
|
const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
|
||||||
|
stdio: "pipe",
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
if (extractResult.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find executable in the extracted tarball
|
||||||
|
const cliPath = join(tempDir, executablePath);
|
||||||
|
|
||||||
|
if (!existsSync(cliPath)) {
|
||||||
|
throw new Error(`Executable not found in extracted tarball at ${cliPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the file executable
|
||||||
|
chmodSync(cliPath, 0o755);
|
||||||
|
|
||||||
|
log.info(`✓ ${owner}/${repo} installed at ${cliPath}`);
|
||||||
|
|
||||||
|
return cliPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a CLI tool from a curl-based install script
|
* Install a CLI tool from a curl-based install script
|
||||||
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const agentsManifest = {
|
|||||||
apiKeyNames: ["google_api_key", "gemini_api_key"],
|
apiKeyNames: ["google_api_key", "gemini_api_key"],
|
||||||
url: "https://ai.google.dev/gemini-api/docs",
|
url: "https://ai.google.dev/gemini-api/docs",
|
||||||
},
|
},
|
||||||
|
opencode: {
|
||||||
|
displayName: "OpenCode",
|
||||||
|
apiKeyNames: ["anthropic_api_key"],
|
||||||
|
url: "https://opencode.ai",
|
||||||
|
},
|
||||||
} as const satisfies Record<string, AgentManifest>;
|
} as const satisfies Record<string, AgentManifest>;
|
||||||
|
|
||||||
// agent name type - union of agent slugs
|
// agent name type - union of agent slugs
|
||||||
|
|||||||
+1
-3
@@ -1,3 +1 @@
|
|||||||
Add an empty commit to https://github.com/pullfrog/pullfrog/pull/5
|
say hello
|
||||||
|
|
||||||
And push it
|
|
||||||
+4
-3
@@ -7,6 +7,7 @@ export interface SpawnOptions {
|
|||||||
input?: string;
|
input?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
stdio?: ("pipe" | "ignore" | "inherit")[];
|
||||||
onStdout?: (chunk: string) => void;
|
onStdout?: (chunk: string) => void;
|
||||||
onStderr?: (chunk: string) => void;
|
onStderr?: (chunk: string) => void;
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@ 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, onStdout, onStderr } = options;
|
const { cmd, args, env, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
@@ -35,7 +36,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
PATH: process.env.PATH || "",
|
PATH: process.env.PATH || "",
|
||||||
HOME: process.env.HOME || "",
|
HOME: process.env.HOME || "",
|
||||||
},
|
},
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: stdio || ["pipe", "pipe", "pipe"],
|
||||||
cwd: cwd || process.cwd(),
|
cwd: cwd || process.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input && child.stdin) {
|
if (input && child.stdin && stdio?.[0] !== "ignore") {
|
||||||
child.stdin.write(input);
|
child.stdin.write(input);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user