From 6f2ccedbf860f950f48547d2b2f1180bd2f0eeb2 Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 14 Nov 2025 14:27:00 -0500 Subject: [PATCH] begin jules support, derive inputs --- agents/index.ts | 2 + agents/jules.ts | 190 ++++++++++++++++++++++++++++++++++++++++++++++++ entry.ts | 5 +- play.ts | 8 +- 4 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 agents/jules.ts diff --git a/agents/index.ts b/agents/index.ts index 553aadf..af18fac 100644 --- a/agents/index.ts +++ b/agents/index.ts @@ -1,9 +1,11 @@ import { claude } from "./claude.ts"; import { codex } from "./codex.ts"; +import { jules } from "./jules.ts"; export const agents = { claude, codex, + jules, } as const; export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKey"]; diff --git a/agents/jules.ts b/agents/jules.ts new file mode 100644 index 0000000..ae10401 --- /dev/null +++ b/agents/jules.ts @@ -0,0 +1,190 @@ +import { log } from "../utils/cli.ts"; +import { parseRepoContext } from "../utils/github.ts"; +import { spawn } from "../utils/subprocess.ts"; +import { addInstructions } from "./instructions.ts"; +import { agent, installFromNpmTarball } from "./shared.ts"; + +export const jules = agent({ + name: "jules", + inputKey: "google_api_key", + install: async () => { + return await installFromNpmTarball({ + packageName: "@google/jules", + version: "latest", + executablePath: "run.cjs", + }); + }, + run: async ({ + prompt, + apiKey, + mcpServers: _mcpServers, + githubInstallationToken: _githubInstallationToken, + cliPath, + }) => { + if (!apiKey) { + throw new Error("google_api_key is required for jules agent"); + } + + const repoContext = parseRepoContext(); + const repoName = `${repoContext.owner}/${repoContext.name}`; + + log.info(`Creating Jules session for ${repoName}...`); + + // Set API key as environment variable for CLI authentication + // Note: The CLI may require browser-based auth via 'jules login' in interactive mode + // In CI, we rely on the API key being set as an environment variable + const env: Record = { + GOOGLE_API_KEY: apiKey, + JULES_API_KEY: apiKey, + }; + // Copy over existing env vars, filtering out undefined values + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + env[key] = value; + } + } + + // Create a new remote session + const sessionPrompt = addInstructions(prompt); + log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`); + + let sessionId: string | undefined; + try { + const createResult = await spawn({ + cmd: "node", + args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt], + env, + onStdout: (chunk) => { + log.info(chunk.trim()); + // Try to extract session ID from output + const match = chunk.match(/session[:\s]+(\d+)/i) || chunk.match(/id[:\s]+(\d+)/i); + if (match && !sessionId) { + sessionId = match[1]; + log.info(`✓ Session ID: ${sessionId}`); + } + }, + onStderr: (chunk) => { + log.warning(chunk.trim()); + }, + }); + + if (createResult.exitCode !== 0) { + throw new Error( + `Failed to create Jules session: ${createResult.stderr || createResult.stdout || "Unknown error"}` + ); + } + + // If session ID wasn't extracted from stdout, try to parse it + if (!sessionId) { + const output = createResult.stdout + createResult.stderr; + const match = output.match(/session[:\s]+(\d+)/i) || output.match(/id[:\s]+(\d+)/i); + if (match) { + sessionId = match[1]; + } + } + + if (!sessionId) { + log.warning("Could not extract session ID from output. Session may have been created."); + log.info(`Output: ${createResult.stdout}`); + } else { + log.info(`✓ Session created: ${sessionId}`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Failed to create Jules session: ${errorMessage}`); + return { + success: false, + error: errorMessage, + output: "", + }; + } + + // Monitor session progress by polling session list + log.info("Monitoring session progress..."); + let finalOutput = ""; + const maxPollAttempts = 300; // ~50 minutes max (10 second intervals) + let pollAttempts = 0; + + while (pollAttempts < maxPollAttempts) { + await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds between polls + pollAttempts++; + + try { + // List sessions to check status + const listResult = await spawn({ + cmd: "node", + args: [cliPath, "remote", "list", "--session"], + env, + onStdout: (chunk) => { + // Log session updates + const trimmed = chunk.trim(); + if (trimmed) { + log.info(trimmed); + } + }, + }); + + if (listResult.exitCode === 0) { + const output = listResult.stdout; + // Check if our session is complete + // The CLI output format may vary, so we look for completion indicators + if (sessionId && output.includes(sessionId)) { + // Try to determine if session is complete + // This is a heuristic - the actual output format may differ + if ( + output.includes("completed") || + output.includes("done") || + output.includes("finished") + ) { + log.info("Session appears to be completed"); + finalOutput = "Session completed. Pulling results..."; + break; + } + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.warning(`Error checking session status: ${errorMessage}`); + } + } + + // Pull results if session ID is available + if (sessionId) { + try { + log.info(`Pulling results for session ${sessionId}...`); + const pullResult = await spawn({ + cmd: "node", + args: [cliPath, "remote", "pull", "--session", sessionId], + env, + onStdout: (chunk) => { + log.info(chunk.trim()); + }, + onStderr: (chunk) => { + log.warning(chunk.trim()); + }, + }); + + if (pullResult.exitCode === 0) { + finalOutput = pullResult.stdout || "Results pulled successfully."; + } else { + log.warning(`Failed to pull results: ${pullResult.stderr || pullResult.stdout}`); + finalOutput = finalOutput || "Session completed. Check Jules dashboard for results."; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.warning(`Error pulling results: ${errorMessage}`); + } + } + + if (pollAttempts >= maxPollAttempts) { + log.warning("Session monitoring timeout reached. Session may still be in progress."); + finalOutput = + finalOutput || "Session monitoring timeout. Check Jules dashboard for session status."; + } + + return { + success: true, + output: finalOutput || "Jules session completed. Check the Jules dashboard for results.", + }; + }, +}); diff --git a/entry.ts b/entry.ts index 732c136..1dde2de 100644 --- a/entry.ts +++ b/entry.ts @@ -5,6 +5,8 @@ */ import * as core from "@actions/core"; +import { flatMorph } from "@ark/util"; +import { agents } from "./agents/index.ts"; import { AgentName, type Inputs, main } from "./main.ts"; import { log } from "./utils/cli.ts"; @@ -20,9 +22,8 @@ async function run(): Promise { try { const inputs: Required = { prompt: core.getInput("prompt", { required: true }), - anthropic_api_key: core.getInput("anthropic_api_key"), - openai_api_key: core.getInput("openai_api_key"), agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined, + ...flatMorph(agents, (_, agent) => [agent.inputKey, core.getInput(agent.inputKey)]), }; const result = await main(inputs); diff --git a/play.ts b/play.ts index ba19f3a..a9d1d37 100644 --- a/play.ts +++ b/play.ts @@ -2,8 +2,10 @@ import { existsSync, readFileSync } from "node:fs"; import { extname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { fromHere } from "@ark/fs"; +import { flatMorph } from "@ark/util"; import arg from "arg"; import { config } from "dotenv"; +import { agents } from "./agents/index.ts"; import { type Inputs, main } from "./main.ts"; import { log } from "./utils/cli.ts"; import { setupTestRepo } from "./utils/setup.ts"; @@ -22,9 +24,11 @@ export async function run( const inputs: Required = { prompt, - openai_api_key: process.env.OPENAI_API_KEY, - anthropic_api_key: process.env.ANTHROPIC_API_KEY, agent: "codex", + ...flatMorph(agents, (_, agent) => [ + agent.inputKey, + process.env[agent.inputKey.toUpperCase()], + ]), }; const result = await main(inputs);