cyrus-gemini-runner
v0.2.5
Published
Gemini CLI process spawning and management for Cyrus
Readme
@cyrus-ai/gemini-runner
A TypeScript wrapper around the Gemini CLI that provides a provider-agnostic interface for running Gemini AI sessions. This package mirrors the architecture of @cyrus-ai/claude-runner while adapting to Gemini CLI's streaming format and session management.
Overview
The gemini-runner package provides two main classes:
- GeminiRunner: Full-featured wrapper around Gemini CLI with streaming support, event handling, and message format conversion
- SimpleGeminiRunner: Simplified interface for enumerated response use cases (yes/no, multiple choice, etc.)
Both classes implement standard interfaces from cyrus-core, making them interchangeable with other AI runner implementations like ClaudeRunner.
Installation
pnpm add @cyrus-ai/gemini-runnerPrerequisites
Gemini CLI: Install the Gemini CLI and ensure it's in your PATH:
# Installation instructions depend on your platform # Verify installation: gemini --versionGemini API Credentials: Configure your Gemini API credentials according to the Gemini CLI documentation.
Quick Start
Basic Usage with GeminiRunner
import { GeminiRunner } from '@cyrus-ai/gemini-runner';
const runner = new GeminiRunner({
cyrusHome: '/path/to/.cyrus',
workingDirectory: '/path/to/project',
model: 'gemini-2.5-flash',
autoApprove: true // Enable --yolo flag
});
// Listen for events
runner.on('message', (message) => {
console.log('New message:', message);
});
runner.on('complete', (messages) => {
console.log('Session complete! Total messages:', messages.length);
});
// Start a session
await runner.start("Analyze this codebase and suggest improvements");
// Get all messages
const messages = runner.getMessages();Simple Enumerated Responses with SimpleGeminiRunner
import { SimpleGeminiRunner } from '@cyrus-ai/gemini-runner';
const runner = new SimpleGeminiRunner({
validResponses: ['yes', 'no', 'maybe'],
cyrusHome: '/path/to/.cyrus',
workingDirectory: '/path/to/project',
model: 'gemini-2.5-flash',
maxTurns: 5,
onProgress: (event) => {
console.log(`Progress: ${event.type}`);
}
});
const result = await runner.query('Is this code correct?');
console.log(`Response: ${result.response}`); // Guaranteed to be 'yes', 'no', or 'maybe'
console.log(`Cost: $${result.cost.toFixed(4)}`);Core Concepts
GeminiRunner
GeminiRunner is the primary class for interacting with the Gemini CLI. It:
- Spawns the Gemini CLI process in headless mode (
--output-format stream-json) - Translates between Gemini CLI's JSON streaming format and Claude SDK message types
- Manages session lifecycle (start, stop, status)
- Provides event-based communication
- Creates detailed logs in both NDJSON and human-readable formats
- Supports both string prompts and streaming prompts
Key Features:
- Provider-agnostic interface: Implements
IAgentRunnerfor compatibility with other runners - Event system: Subscribe to
message,error,complete, andstreamEventevents - Streaming support: Add messages incrementally with
addStreamMessage() - Session management: Track session state with
isRunning()andgetMessages() - MCP server support: Configure Model Context Protocol servers (inherited from
IAgentRunner)
SimpleGeminiRunner
SimpleGeminiRunner extends SimpleAgentRunner to provide a streamlined interface for enumerated response scenarios:
- Validates responses against a predefined set of valid answers
- Handles timeout management automatically
- Cleans responses (removes markdown, code blocks, quotes)
- Emits progress events (thinking, tool-use, validating)
- Builds system prompts with valid response constraints
- Extracts cost information from result messages
Use Cases:
- Yes/No questions
- Multiple choice selection
- Status confirmations
- Classification tasks
API Reference
GeminiRunner
Constructor Options
interface GeminiRunnerConfig extends IAgentRunnerConfig {
// Gemini-specific options
geminiPath?: string; // Path to gemini CLI (default: 'gemini')
autoApprove?: boolean; // Enable --yolo flag (default: false)
approvalMode?: 'auto_edit' | 'auto' | 'manual'; // Approval mode
debug?: boolean; // Enable debug output
// Inherited from IAgentRunnerConfig
model?: string; // Model to use (e.g., 'gemini-2.5-flash')
cyrusHome: string; // Home directory for logs
workingDirectory: string; // Working directory for Gemini
systemPrompt?: string; // System prompt for session
mcpServers?: MCPServerConfig[]; // MCP server configurations
env?: Record<string, string>; // Environment variables
}Methods
start(prompt: string): Promise
- Start a new session with a string prompt
- Throws if a session is already running
startStreaming(initialPrompt?: string): Promise
- Start a new session with streaming input capability
- Optionally provide an initial prompt
- Use
addStreamMessage()to add more messages - Call
completeStream()when done
addStreamMessage(content: string): void
- Add a message to an active streaming session
- Must be called after
startStreaming()
completeStream(): void
- Signal the end of streaming input
- Session will continue processing
stop(): Promise
- Terminate the current session
- Kills the Gemini CLI process
getMessages(): SDKMessage[]
- Retrieve all messages from the current session
- Returns both user and assistant messages
isRunning(): boolean
- Check if a session is currently active
Events
'message': (message: SDKMessage) => void
- Emitted when a new message is received from Gemini
'error': (error: Error) => void
- Emitted when an error occurs
'complete': (messages: SDKMessage[]) => void
- Emitted when the session completes successfully
'streamEvent': (event: GeminiStreamEvent) => void
- Emitted for raw Gemini CLI events (for debugging)
SimpleGeminiRunner
Constructor Options
interface SimpleGeminiRunnerConfig {
validResponses: string[]; // REQUIRED: Valid response options
cyrusHome: string; // REQUIRED: Home directory for logs
workingDirectory?: string; // Working directory (default: cwd)
model?: string; // Model to use (default: 'gemini-2.5-flash')
systemPrompt?: string; // Additional system prompt
maxTurns?: number; // Max conversation turns (default: 10)
timeout?: number; // Timeout in ms (default: 300000)
onProgress?: (event: SimpleAgentProgressEvent) => void;
mcpServers?: MCPServerConfig[];
env?: Record<string, string>;
}Methods
query(prompt: string, options?: SimpleAgentQueryOptions): Promise
- Execute a query and get a validated response
- Returns an object with
response,cost, andmessages
interface SimpleAgentResult {
response: string; // Validated response (one of validResponses)
cost: number; // Total cost in dollars
messages: SDKMessage[]; // All messages from session
}Adapter Pattern
The package uses an adapter pattern to translate between Gemini CLI's JSON streaming format and Claude SDK message types. This allows the runner to integrate seamlessly with other parts of the Cyrus ecosystem.
Message Format Conversion
Gemini Stream Events → SDK Messages:
GeminiInitEvent→ Session ID extractionGeminiMessageEvent(user) →SDKUserMessageGeminiMessageEvent(assistant) →SDKAssistantMessageGeminiToolUseEvent→SDKAssistantMessagewithtool_usecontent blockGeminiToolResultEvent→SDKUserMessagewithtool_resultcontent blockGeminiResultEvent(success) →SDKAssistantMessagewith final response
Key Differences from Claude:
- Gemini uses simple
{type, role, content}for messages - SDK uses
{type, message: {role, content}, session_id, ...}structure - Tool use IDs are generated client-side for Gemini (
${tool_name}_${timestamp}) - Session ID is "pending" until init event is received from Gemini CLI
Differences from ClaudeRunner
While GeminiRunner mirrors the architecture of ClaudeRunner, there are some key differences:
| Aspect | ClaudeRunner | GeminiRunner |
|--------|--------------|--------------|
| CLI Command | claude --output-format ndjson | gemini --output-format stream-json |
| Session ID | Generated client-side or via --continue | Assigned by CLI in init event |
| Stream Format | NDJSON with SDK-compatible messages | Custom JSON format requiring adapter |
| Tool Use IDs | Native SDK format | Generated client-side |
| Auto-Approval | Approval callbacks or flags | --yolo flag and --approval-mode |
| System Prompts | Native CLI support | Accepted in config (CLI handling TBD) |
Supported Models
The package supports Gemini 2.5 and later models. Common model identifiers:
gemini-2.5-flash- Fast, cost-effective modelgemini-2.5-pro- Advanced reasoning and complex tasks- Additional models as supported by the Gemini CLI
Specify the model in the configuration:
const runner = new GeminiRunner({
model: 'gemini-2.5-flash',
// ... other options
});Configuration
Environment Variables
The package respects standard Gemini CLI environment variables for API credentials. Refer to the Gemini CLI documentation for setup instructions.
Log Files
Both runners create detailed logs in the cyrusHome directory:
{cyrusHome}/logs/{workspaceName}/{sessionId}.ndjson- NDJSON event log{cyrusHome}/logs/{workspaceName}/{sessionId}.log- Human-readable log
MCP Server Configuration
Configure Model Context Protocol servers for enhanced capabilities:
const runner = new GeminiRunner({
cyrusHome: '/path/to/.cyrus',
workingDirectory: '/path/to/project',
mcpServers: [
{
name: 'linear',
type: 'http',
url: 'https://mcp.linear.app',
headers: {
'Authorization': 'Bearer YOUR_TOKEN'
}
}
]
});Examples
See the examples/ directory for complete working examples:
basic-usage.ts- Basic GeminiRunner usage with event handlingsimple-agent-example.ts- SimpleGeminiRunner for enumerated responses
To run the examples:
cd packages/gemini-runner
pnpm build
node examples/basic-usage.js # Or .ts with ts-nodeTroubleshooting
Common Issues
"gemini: command not found"
- Ensure the Gemini CLI is installed and in your PATH
- Or specify the full path in the config:
geminiPath: '/path/to/gemini'
"Session already running"
- You can only have one active session per GeminiRunner instance
- Call
stop()before starting a new session, or create a new instance
"Invalid response from Gemini"
- Check that your validResponses match the expected output format
- Use the
onProgresscallback to see raw responses during validation - Verify the system prompt is being respected by the model
Empty messages array
- The session may have errored before producing any messages
- Check the
errorevent for details - Review the log files in
{cyrusHome}/logs/
Response validation fails with SimpleGeminiRunner
- Increase
maxTurnsto allow more attempts - Simplify your
validResponseslist - Check if the model is producing markdown or code blocks (automatically cleaned)
- Use
onProgressto debug what responses are being received
Timeout errors
- Increase the
timeoutoption (default: 300000ms / 5 minutes) - Check if the Gemini CLI is hanging on approval prompts (use
autoApprove: true) - Verify your working directory is accessible and doesn't have permission issues
Debug Mode
Enable debug mode for detailed logging:
const runner = new GeminiRunner({
debug: true,
// ... other options
});Log Analysis
Check the NDJSON log files for detailed event information:
cat ~/.cyrus/logs/my-workspace/{session-id}.ndjson | jq .Or the human-readable log:
cat ~/.cyrus/logs/my-workspace/{session-id}.logDevelopment
Building
pnpm buildRunning Tests
pnpm test # Watch mode
pnpm test:run # Run onceType Checking
pnpm typecheckRelated Packages
@cyrus-ai/core- Core types and interfaces@cyrus-ai/claude-runner- Claude CLI wrapper (similar architecture)@cyrus-ai/simple-agent-runner- Abstract base for simple enumerated responses
License
MIT
Support
For issues and feature requests, please use the project's issue tracker.
