download gemini from Github

This commit is contained in:
Shawn Morreau
2025-11-21 14:11:20 -05:00
committed by GitHub
8 changed files with 114 additions and 49 deletions
+1 -9
View File
@@ -21,21 +21,16 @@ export const cursor = agent({
configureCursorMcpServers({ mcpServers, cliPath }); configureCursorMcpServers({ mcpServers, cliPath });
try { try {
// Run cursor-agent in non-interactive mode with the prompt
// Using -p flag for prompt, --output-format text for plain text output
// and --approve-mcps to automatically approve all MCP servers
const fullPrompt = addInstructions(payload); const fullPrompt = addInstructions(payload);
log.info("Running Cursor CLI..."); log.info("Running Cursor CLI...");
// Use spawn to handle streaming output
// Use --print flag explicitly for non-interactive mode
return new Promise((resolve) => { return new Promise((resolve) => {
const child = spawn( const child = spawn(
cliPath, cliPath,
["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"], ["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"],
{ {
cwd: process.cwd(), // Run in current working directory cwd: process.cwd(),
env: { env: {
...process.env, ...process.env,
CURSOR_API_KEY: apiKey, CURSOR_API_KEY: apiKey,
@@ -50,7 +45,6 @@ export const cursor = agent({
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
// Log when process starts
child.on("spawn", () => { child.on("spawn", () => {
log.debug("Cursor CLI process spawned"); log.debug("Cursor CLI process spawned");
}); });
@@ -65,12 +59,10 @@ export const cursor = agent({
child.stderr?.on("data", (data) => { child.stderr?.on("data", (data) => {
const text = data.toString(); const text = data.toString();
stderr += text; stderr += text;
// Log errors as they come - but also write to stdout so we can see it
process.stderr.write(text); process.stderr.write(text);
log.warning(text); log.warning(text);
}); });
// Handle process exit
child.on("close", (code, signal) => { child.on("close", (code, signal) => {
if (signal) { if (signal) {
log.warning(`Cursor CLI terminated by signal: ${signal}`); log.warning(`Cursor CLI terminated by signal: ${signal}`);
+7 -8
View File
@@ -2,16 +2,16 @@ import { spawnSync } from "node:child_process";
import { log } from "../utils/cli.ts"; import { log } from "../utils/cli.ts";
import { spawn } from "../utils/subprocess.ts"; import { spawn } from "../utils/subprocess.ts";
import { addInstructions } from "./instructions.ts"; import { addInstructions } from "./instructions.ts";
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts"; import { agent, type ConfigureMcpServersParams, installFromGithub } from "./shared.ts";
export const gemini = agent({ export const gemini = agent({
name: "gemini", name: "gemini",
install: async () => { install: async () => {
return await installFromNpmTarball({ return await installFromGithub({
packageName: "@google/gemini-cli", owner: "google-gemini",
version: "latest", repo: "gemini-cli",
executablePath: "dist/index.js", tag: "v0.16.0",
installDependencies: true, assetName: "gemini.js",
}); });
}, },
run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => { run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
@@ -31,7 +31,7 @@ export const gemini = agent({
try { try {
const result = await spawn({ const result = await spawn({
cmd: "node", cmd: "node",
args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt], args: [cliPath, "--yolo", "--output-format=text", "-p", sessionPrompt],
env: { env: {
GEMINI_API_KEY: apiKey, GEMINI_API_KEY: apiKey,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken, GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
@@ -93,7 +93,6 @@ function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersP
const addArgs = ["mcp", "add", serverName, command, ...args]; const addArgs = ["mcp", "add", serverName, command, ...args];
// Add environment variables as --env flags
for (const [key, value] of Object.entries(envVars)) { for (const [key, value] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value}`); addArgs.push("--env", `${key}=${value}`);
} }
+98
View File
@@ -1,5 +1,7 @@
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { chmodSync, createWriteStream, existsSync } from "node:fs"; import { chmodSync, createWriteStream, existsSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { pipeline } from "node:stream/promises"; import { pipeline } from "node:stream/promises";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk"; import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
@@ -54,6 +56,17 @@ export interface InstallFromCurlParams {
executableName: string; executableName: string;
} }
/**
* Parameters for installing from GitHub releases
*/
export interface InstallFromGithubParams {
owner: string;
repo: string;
tag?: string;
assetName?: string;
executablePath?: string;
}
/** /**
* NPM registry response data structure * NPM registry response data structure
*/ */
@@ -167,6 +180,91 @@ export async function installFromNpmTarball({
return cliPath; return cliPath;
} }
/**
* Install a CLI tool from GitHub releases
* Downloads the latest release asset from GitHub and returns the path to the executable
* The temp directory will be cleaned up by the OS automatically
*/
export async function installFromGithub({
owner,
repo,
tag,
assetName,
executablePath,
}: InstallFromGithubParams): Promise<string> {
log.info(`📦 Installing ${owner}/${repo} from GitHub releases...`);
// fetch release from GitHub API (specific tag or latest)
const releaseUrl = tag
? `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`
: `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
log.info(`Fetching release from ${releaseUrl}...`);
const releaseResponse = await fetch(releaseUrl);
if (!releaseResponse.ok) {
throw new Error(
`Failed to fetch release: ${releaseResponse.status} ${releaseResponse.statusText}`
);
}
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}...`);
// create temp directory
const tempDirPrefix = `${owner}-${repo}-github-`;
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
// determine file extension and download path
const urlPath = new URL(assetUrl).pathname;
const fileName = urlPath.split("/").pop() || "asset";
const downloadPath = join(tempDir, fileName);
// download the asset
const assetResponse = await fetch(assetUrl);
if (!assetResponse.ok) {
throw new Error(
`Failed to download asset: ${assetResponse.status} ${assetResponse.statusText}`
);
}
if (!assetResponse.body) throw new Error("Response body is null");
const fileStream = createWriteStream(downloadPath);
await pipeline(assetResponse.body, fileStream);
log.info(`Downloaded asset to ${downloadPath}`);
// determine the executable path
let cliPath: string;
if (executablePath) {
cliPath = join(tempDir, executablePath);
} else {
// no executablePath, assume the downloaded file is the executable
cliPath = downloadPath;
}
if (!existsSync(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`);
}
chmodSync(cliPath, 0o755);
log.info(`✓ Installed from GitHub release 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
+1 -3
View File
@@ -1,3 +1 @@
Print the MCP tools available to you. write a comment to https://github.com/pullfrogai/scratch/pull/29 that tells a joke
Try to call select_mode with the name "Plan".
Then tell me a joke
+3 -8
View File
@@ -135,7 +135,6 @@ To fix this, add the required secret to your GitHub repository:
interface MainContext { interface MainContext {
inputs: Inputs; inputs: Inputs;
githubInstallationToken: string; githubInstallationToken: string;
tokenToRevoke: string | null;
repoContext: RepoContext; repoContext: RepoContext;
agentName: AgentNameType; agentName: AgentNameType;
agent: (typeof agents)[AgentNameType]; agent: (typeof agents)[AgentNameType];
@@ -155,16 +154,14 @@ async function initializeContext(
Inputs.assert(inputs); Inputs.assert(inputs);
setupGitConfig(); setupGitConfig();
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken(); const githubInstallationToken = await setupGitHubInstallationToken();
const tokenToRevoke = wasAcquired ? githubInstallationToken : null;
const repoContext = parseRepoContext(); const repoContext = parseRepoContext();
return { return {
inputs, inputs,
githubInstallationToken, githubInstallationToken,
tokenToRevoke,
repoContext, repoContext,
agentName: "claude" as AgentNameType, agentName: "claude",
agent: agents.claude, agent: agents.claude,
sharedTempDir: "", sharedTempDir: "",
mcpLogPath: "", mcpLogPath: "",
@@ -292,7 +289,5 @@ async function cleanup(ctx: Omit<MainContext, "mcpServers" | "cliPath" | "apiKey
if (ctx.pollInterval) { if (ctx.pollInterval) {
clearInterval(ctx.pollInterval); clearInterval(ctx.pollInterval);
} }
if (ctx.tokenToRevoke) { await revokeInstallationToken(ctx.githubInstallationToken);
await revokeInstallationToken(ctx.tokenToRevoke);
}
} }
+1 -1
View File
@@ -2,6 +2,6 @@ import { configure } from "arktype/config";
configure({ configure({
toJsonSchema: { toJsonSchema: {
// dialect: null, dialect: null,
}, },
}); });
+1 -1
View File
@@ -27,7 +27,7 @@ export async function run(prompt: string): Promise<AgentResult> {
// we don't need to extract it here since main() will parse the payload // we don't need to extract it here since main() will parse the payload
const inputs: Required<Inputs> = { const inputs: Required<Inputs> = {
prompt, prompt,
defaultAgent: "cursor", defaultAgent: "gemini",
...flatMorph(agents, (_, agent) => ...flatMorph(agents, (_, agent) =>
agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
), ),
+2 -19
View File
@@ -43,12 +43,6 @@ interface RepositoriesResponse {
repositories: Repository[]; repositories: Repository[];
} }
function checkExistingToken(): string | null {
const inputToken = core.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
return inputToken || envToken || null;
}
function isGitHubActionsEnvironment(): boolean { function isGitHubActionsEnvironment(): boolean {
return Boolean(process.env.GITHUB_ACTIONS); return Boolean(process.env.GITHUB_ACTIONS);
} }
@@ -249,24 +243,13 @@ async function acquireNewToken(): Promise<string> {
/** /**
* Setup GitHub installation token for the action * Setup GitHub installation token for the action
* Returns the token and whether it was acquired (needs revocation)
*/ */
export async function setupGitHubInstallationToken(): Promise<{ export async function setupGitHubInstallationToken(): Promise<string> {
githubInstallationToken: string;
wasAcquired: boolean;
}> {
const existingToken = checkExistingToken();
if (existingToken) {
core.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return { githubInstallationToken: existingToken, wasAcquired: false };
}
const acquiredToken = await acquireNewToken(); const acquiredToken = await acquireNewToken();
core.setSecret(acquiredToken); core.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken; process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
return { githubInstallationToken: acquiredToken, wasAcquired: true }; return acquiredToken;
} }
/** /**