Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
2025-04-15 21:41:08 -07:00
import {
GenerateContentConfig , GoogleGenAI , Part , Chat ,
Type ,
SchemaUnion ,
PartListUnion ,
Content
} from '@google/genai' ;
import { getApiKey } from '../config/env.js' ;
import { CoreSystemPrompt } from './prompts.js' ;
import { type ToolCallEvent , type ToolCallConfirmationDetails , ToolCallStatus } from '../ui/types.js' ;
import process from 'node:process' ;
import { toolRegistry } from '../tools/tool-registry.js' ;
2025-04-17 17:25:01 -04:00
import { ToolResult } from '../tools/tools.js' ;
Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
2025-04-15 21:41:08 -07:00
import { getFolderStructure } from '../utils/getFolderStructure.js' ;
2025-04-17 12:03:02 -07:00
import { GeminiEventType , GeminiStream } from './gemini-stream.js' ;
Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
2025-04-15 21:41:08 -07:00
type ToolExecutionOutcome = {
callId : string ;
name : string ;
args : Record < string , any > ;
result? : ToolResult ;
error? : any ;
confirmationDetails? : ToolCallConfirmationDetails ;
} ;
export class GeminiClient {
private ai : GoogleGenAI ;
private defaultHyperParameters : GenerateContentConfig = {
temperature : 0 ,
topP : 1 ,
} ;
private readonly MAX_TURNS = 100 ;
constructor ( ) {
const apiKey = getApiKey ( ) ;
this . ai = new GoogleGenAI ( { apiKey } ) ;
}
public async startChat ( ) : Promise < Chat > {
const tools = toolRegistry . getToolSchemas ( ) ;
// --- Get environmental information ---
const cwd = process . cwd ( ) ;
const today = new Date ( ) . toLocaleDateString ( undefined , { // Use locale-aware date formatting
weekday : 'long' , year : 'numeric' , month : 'long' , day : 'numeric'
} ) ;
const platform = process . platform ;
// --- Format information into a conversational multi-line string ---
const folderStructure = await getFolderStructure ( cwd ) ;
// --- End folder structure formatting ---)
const initialContextText = `
Okay , just setting up the context for our chat .
Today is $ { today } .
My operating system is : $ { platform }
I ' m currently working in the directory : $ { cwd }
$ { folderStructure }
` .trim();
const initialContextPart : Part = { text : initialContextText } ;
// --- End environmental information formatting ---
try {
const chat = this . ai . chats . create ( {
2025-04-17 12:03:02 -07:00
model : 'gemini-2.0-flash' , //'gemini-2.0-flash',
Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
2025-04-15 21:41:08 -07:00
config : {
systemInstruction : CoreSystemPrompt ,
. . . this . defaultHyperParameters ,
tools ,
} ,
history : [
// --- Add the context as a single part in the initial user message ---
{
role : "user" ,
parts : [ initialContextPart ] // Pass the single Part object in an array
} ,
// --- Add an empty model response to balance the history ---
{
role : "model" ,
parts : [ { text : "Got it. Thanks for the context!" } ] // A slightly more conversational model response
}
// --- End history modification ---
] ,
} ) ;
return chat ;
} catch ( error ) {
console . error ( "Error initializing Gemini chat session:" , error ) ;
const message = error instanceof Error ? error . message : "Unknown error." ;
throw new Error ( ` Failed to initialize chat: ${ message } ` ) ;
}
}
public addMessageToHistory ( chat : Chat , message : Content ) : void {
const history = chat . getHistory ( ) ;
history . push ( message ) ;
this . ai . chats
chat
}
public async * sendMessageStream (
chat : Chat ,
request : PartListUnion ,
signal? : AbortSignal
) : GeminiStream {
let currentMessageToSend : PartListUnion = request ;
let turns = 0 ;
try {
while ( turns < this . MAX_TURNS ) {
turns ++ ;
const resultStream = await chat . sendMessageStream ( { message : currentMessageToSend } ) ;
let functionResponseParts : Part [ ] = [ ] ;
let pendingToolCalls : Array < { callId : string ; name : string ; args : Record < string , any > } > = [ ] ;
let yieldedTextInTurn = false ;
const chunksForDebug = [ ] ;
for await ( const chunk of resultStream ) {
chunksForDebug . push ( chunk ) ;
if ( signal ? . aborted ) {
const abortError = new Error ( "Request cancelled by user during stream." ) ;
abortError . name = 'AbortError' ;
throw abortError ;
}
const functionCalls = chunk . functionCalls ;
if ( functionCalls && functionCalls . length > 0 ) {
for ( const call of functionCalls ) {
const callId = call . id ? ? ` ${ call . name } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ;
const name = call . name || 'undefined_tool_name' ;
const args = ( call . args || { } ) as Record < string , any > ;
pendingToolCalls . push ( { callId , name , args } ) ;
const evtValue : ToolCallEvent = {
type : 'tool_call' ,
status : ToolCallStatus.Pending ,
callId ,
name ,
args ,
resultDisplay : undefined ,
confirmationDetails : undefined ,
}
yield {
type : GeminiEventType . ToolCallInfo ,
value : evtValue ,
} ;
}
} else {
const text = chunk . text ;
if ( text ) {
yieldedTextInTurn = true ;
yield {
type : GeminiEventType . Content ,
value : text ,
} ;
}
}
}
if ( pendingToolCalls . length > 0 ) {
const toolPromises : Promise < ToolExecutionOutcome > [ ] = pendingToolCalls . map ( async pendingToolCall = > {
const tool = toolRegistry . getTool ( pendingToolCall . name ) ;
if ( ! tool ) {
// Directly return error outcome if tool not found
return { . . . pendingToolCall , error : new Error ( ` Tool " ${ pendingToolCall . name } " not found or is not registered. ` ) } ;
}
try {
const confirmation = await tool . shouldConfirmExecute ( pendingToolCall . args ) ;
if ( confirmation ) {
return { . . . pendingToolCall , confirmationDetails : confirmation } ;
}
} catch ( error ) {
return { . . . pendingToolCall , error : new Error ( ` Tool failed to check tool confirmation: ${ error } ` ) } ;
}
try {
const result = await tool . execute ( pendingToolCall . args ) ;
return { . . . pendingToolCall , result } ;
} catch ( error ) {
return { . . . pendingToolCall , error : new Error ( ` Tool failed to execute: ${ error } ` ) } ;
}
} ) ;
const toolExecutionOutcomes : ToolExecutionOutcome [ ] = await Promise . all ( toolPromises ) ;
for ( const executedTool of toolExecutionOutcomes ) {
const { callId , name , args , result , error , confirmationDetails } = executedTool ;
if ( error ) {
const errorMessage = error ? . message || String ( error ) ;
yield {
type : GeminiEventType . Content ,
value : ` [Error invoking tool ${ name } : ${ errorMessage } ] ` ,
} ;
} else if ( result && typeof result === 'object' && result !== null && 'error' in result ) {
const errorMessage = String ( result . error ) ;
yield {
type : GeminiEventType . Content ,
value : ` [Error executing tool ${ name } : ${ errorMessage } ] ` ,
} ;
} else {
const status = confirmationDetails ? ToolCallStatus.Confirming : ToolCallStatus.Invoked ;
const evtValue : ToolCallEvent = { type : 'tool_call' , status , callId , name , args , resultDisplay : result?.returnDisplay , confirmationDetails }
yield {
type : GeminiEventType . ToolCallInfo ,
value : evtValue ,
} ;
}
}
pendingToolCalls = [ ] ;
const waitingOnConfirmations = toolExecutionOutcomes . filter ( outcome = > outcome . confirmationDetails ) . length > 0 ;
if ( waitingOnConfirmations ) {
// Stop processing content, wait for user.
// TODO: Kill token processing once API supports signals.
break ;
}
functionResponseParts = toolExecutionOutcomes . map ( ( executedTool : ToolExecutionOutcome ) : Part = > {
const { name , result , error } = executedTool ;
const output = { "output" : result ? . llmContent } ;
let toolOutcomePayload : any ;
if ( error ) {
const errorMessage = error ? . message || String ( error ) ;
toolOutcomePayload = { error : ` Invocation failed: ${ errorMessage } ` } ;
console . error ( ` [Turn ${ turns } ] Critical error invoking tool ${ name } : ` , error ) ;
} else if ( result && typeof result === 'object' && result !== null && 'error' in result ) {
toolOutcomePayload = output ;
console . warn ( ` [Turn ${ turns } ] Tool ${ name } returned an error structure: ` , result . error ) ;
} else {
toolOutcomePayload = output ;
}
return {
functionResponse : {
name : name ,
id : executedTool.callId ,
response : toolOutcomePayload ,
} ,
} ;
} ) ;
currentMessageToSend = functionResponseParts ;
} else if ( yieldedTextInTurn ) {
const history = chat . getHistory ( ) ;
const checkPrompt = ` Analyze *only* the content and structure of your immediately preceding response (your last turn in the conversation history). Based *strictly* on that response, determine who should logically speak next: the 'user' or the 'model' (you).
* * Decision Rules ( apply in order ) : * *
1 . * * Model Continues : * * If your last response explicitly states an immediate next action * you * intend to take ( e . g . , "Next, I will..." , "Now I'll process..." , "Moving on to analyze..." , indicates an intended tool call that didn 't execute), OR if the response seems clearly incomplete (cut off mid-thought without a natural conclusion), then the **' model ' * * should speak next .
2 . * * Question to User : * * If your last response ends with a direct question specifically addressed * to the user * , then the * * 'user' * * should speak next .
3 . * * Waiting for User : * * If your last response completed a thought , statement , or task * and * does not meet the criteria for Rule 1 ( Model Continues ) or Rule 2 ( Question to User ) , it implies a pause expecting user input or reaction . In this case , the * * 'user' * * should speak next .
* * Output Format : * *
Respond * only * in JSON format according to the following schema . Do not include any text outside the JSON structure .
\ ` \` \` json
{
"type" : "object" ,
"properties" : {
"reasoning" : {
"type" : "string" ,
"description" : "Brief explanation justifying the 'next_speaker' choice based *strictly* on the applicable rule and the content/structure of the preceding turn."
} ,
"next_speaker" : {
"type" : "string" ,
"enum" : [ "user" , "model" ] ,
"description" : "Who should speak next based *only* on the preceding turn and the decision rules."
}
} ,
"required" : [ "next_speaker" , "reasoning" ]
\ ` \` \`
} ` ;
// Schema Idea
const responseSchema : SchemaUnion = {
type : Type . OBJECT ,
properties : {
reasoning : {
type : Type . STRING ,
description : "Brief explanation justifying the 'next_speaker' choice based *strictly* on the applicable rule and the content/structure of the preceding turn."
} ,
next_speaker : {
type : Type . STRING ,
enum : [ 'user' , 'model' ] , // Enforce the choices
description : "Who should speak next based *only* on the preceding turn and the decision rules" ,
} ,
} ,
required : [ 'reasoning' , 'next_speaker' ]
} ;
try {
// Use the new generateJson method, passing the history and the check prompt
const parsedResponse = await this . generateJson ( [ . . . history , { role : "user" , parts : [ { text : checkPrompt } ] } ] , responseSchema ) ;
// Safely extract the next speaker value
const nextSpeaker : string | undefined = typeof parsedResponse ? . next_speaker === 'string' ? parsedResponse.next_speaker : undefined ;
if ( nextSpeaker === 'model' ) {
currentMessageToSend = { text : 'alright' } ; // Or potentially a more meaningful continuation prompt
} else {
// 'user' should speak next, or value is missing/invalid. End the turn.
break ;
}
} catch ( error ) {
console . error ( ` [Turn ${ turns } ] Failed to get or parse next speaker check: ` , error ) ;
// If the check fails, assume user should speak next to avoid infinite loops
break ;
}
} else {
console . warn ( ` [Turn ${ turns } ] No text or function calls received from Gemini. Ending interaction. ` ) ;
break ;
}
}
if ( turns >= this . MAX_TURNS ) {
console . warn ( "sendMessageStream: Reached maximum tool call turns limit." ) ;
yield {
type : GeminiEventType . Content ,
value : "\n\n[System Notice: Maximum interaction turns reached. The conversation may be incomplete.]" ,
} ;
}
} catch ( error : unknown ) {
if ( error instanceof Error && error . name === 'AbortError' ) {
console . log ( "Gemini stream request aborted by user." ) ;
throw error ;
} else {
console . error ( ` Error during Gemini stream or tool interaction: ` , error ) ;
const message = error instanceof Error ? error.message : String ( error ) ;
yield {
type : GeminiEventType . Content ,
value : ` \ n \ n[Error: An unexpected error occurred during the chat: ${ message } ] ` ,
} ;
throw error ;
}
}
}
/ * *
* Generates structured JSON content based on conversational history and a schema .
* @param contents The conversational history ( Content array ) to provide context .
* @param schema The SchemaUnion defining the desired JSON structure .
* @returns A promise that resolves to the parsed JSON object matching the schema .
* @throws Throws an error if the API call fails or the response is not valid JSON .
* /
public async generateJson ( contents : Content [ ] , schema : SchemaUnion ) : Promise < any > {
try {
const result = await this . ai . models . generateContent ( {
model : 'gemini-2.0-flash' , // Using flash for potentially faster structured output
config : {
. . . this . defaultHyperParameters ,
systemInstruction : CoreSystemPrompt ,
responseSchema : schema ,
responseMimeType : 'application/json' ,
} ,
contents : contents , // Pass the full Content array
} ) ;
const responseText = result . text ;
if ( ! responseText ) {
throw new Error ( "API returned an empty response." ) ;
}
try {
const parsedJson = JSON . parse ( responseText ) ;
// TODO: Add schema validation if needed
return parsedJson ;
} catch ( parseError ) {
console . error ( "Failed to parse JSON response:" , responseText ) ;
throw new Error ( ` Failed to parse API response as JSON: ${ parseError instanceof Error ? parseError.message : String ( parseError ) } ` ) ;
}
} catch ( error ) {
console . error ( "Error generating JSON content:" , error ) ;
const message = error instanceof Error ? error . message : "Unknown API error." ;
throw new Error ( ` Failed to generate JSON content: ${ message } ` ) ;
}
}
}