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 });
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);
log.info("Running Cursor CLI...");
// Use spawn to handle streaming output
// Use --print flag explicitly for non-interactive mode
return new Promise((resolve) => {
const child = spawn(
cliPath,
["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"],
{
cwd: process.cwd(), // Run in current working directory
cwd: process.cwd(),
env: {
...process.env,
CURSOR_API_KEY: apiKey,
@@ -50,7 +45,6 @@ export const cursor = agent({
let stdout = "";
let stderr = "";
// Log when process starts
child.on("spawn", () => {
log.debug("Cursor CLI process spawned");
});
@@ -65,12 +59,10 @@ export const cursor = agent({
child.stderr?.on("data", (data) => {
const text = data.toString();
stderr += text;
// Log errors as they come - but also write to stdout so we can see it
process.stderr.write(text);
log.warning(text);
});
// Handle process exit
child.on("close", (code, signal) => {
if (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 { spawn } from "../utils/subprocess.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({
name: "gemini",
install: async () => {
return await installFromNpmTarball({
packageName: "@google/gemini-cli",
version: "latest",
executablePath: "dist/index.js",
installDependencies: true,
return await installFromGithub({
owner: "google-gemini",
repo: "gemini-cli",
tag: "v0.16.0",
assetName: "gemini.js",
});
},
run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
@@ -31,7 +31,7 @@ export const gemini = agent({
try {
const result = await spawn({
cmd: "node",
args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt],
args: [cliPath, "--yolo", "--output-format=text", "-p", sessionPrompt],
env: {
GEMINI_API_KEY: apiKey,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
@@ -93,7 +93,6 @@ function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersP
const addArgs = ["mcp", "add", serverName, command, ...args];
// Add environment variables as --env flags
for (const [key, value] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value}`);
}
+98
View File
@@ -1,5 +1,7 @@
import { spawnSync } from "node:child_process";
import { chmodSync, createWriteStream, existsSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
@@ -54,6 +56,17 @@ export interface InstallFromCurlParams {
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
*/
@@ -167,6 +180,91 @@ export async function installFromNpmTarball({
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
* 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.
Try to call select_mode with the name "Plan".
Then tell me a joke
write a comment to https://github.com/pullfrogai/scratch/pull/29 that tells a joke
+3 -8
View File
@@ -135,7 +135,6 @@ To fix this, add the required secret to your GitHub repository:
interface MainContext {
inputs: Inputs;
githubInstallationToken: string;
tokenToRevoke: string | null;
repoContext: RepoContext;
agentName: AgentNameType;
agent: (typeof agents)[AgentNameType];
@@ -155,16 +154,14 @@ async function initializeContext(
Inputs.assert(inputs);
setupGitConfig();
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
const tokenToRevoke = wasAcquired ? githubInstallationToken : null;
const githubInstallationToken = await setupGitHubInstallationToken();
const repoContext = parseRepoContext();
return {
inputs,
githubInstallationToken,
tokenToRevoke,
repoContext,
agentName: "claude" as AgentNameType,
agentName: "claude",
agent: agents.claude,
sharedTempDir: "",
mcpLogPath: "",
@@ -292,7 +289,5 @@ async function cleanup(ctx: Omit<MainContext, "mcpServers" | "cliPath" | "apiKey
if (ctx.pollInterval) {
clearInterval(ctx.pollInterval);
}
if (ctx.tokenToRevoke) {
await revokeInstallationToken(ctx.tokenToRevoke);
}
await revokeInstallationToken(ctx.githubInstallationToken);
}
+1 -1
View File
@@ -2,6 +2,6 @@ import { configure } from "arktype/config";
configure({
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
const inputs: Required<Inputs> = {
prompt,
defaultAgent: "cursor",
defaultAgent: "gemini",
...flatMorph(agents, (_, agent) =>
agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
),
+2 -19
View File
@@ -43,12 +43,6 @@ interface RepositoriesResponse {
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 {
return Boolean(process.env.GITHUB_ACTIONS);
}
@@ -249,24 +243,13 @@ async function acquireNewToken(): Promise<string> {
/**
* Setup GitHub installation token for the action
* Returns the token and whether it was acquired (needs revocation)
*/
export async function setupGitHubInstallationToken(): Promise<{
githubInstallationToken: string;
wasAcquired: boolean;
}> {
const existingToken = checkExistingToken();
if (existingToken) {
core.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return { githubInstallationToken: existingToken, wasAcquired: false };
}
export async function setupGitHubInstallationToken(): Promise<string> {
const acquiredToken = await acquireNewToken();
core.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
return { githubInstallationToken: acquiredToken, wasAcquired: true };
return acquiredToken;
}
/**