pub mod agent;
pub mod commands;
pub mod config;
pub mod config_store;
pub mod prompt;
pub mod query;
pub mod session;
pub mod tools;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use colored::*;
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use crate::config::AppConfig;
use crate::session::Session;
use crate::agent::Agent;
use crate::query::{QueryEngine, ModelProvider, ProviderType, create_provider};
use crate::tools::ToolRegistry;
#[derive(Parser, Debug)]
#[command(
name = "Gigi",
about = "Gigi — A Claude Code-like AI coding assistant CLI in Rust",
version = "1.0.0"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long, short = 'p')]
provider: Option<String>,
#[arg(long, short = 'm')]
model: Option<String>,
#[arg(long, short = 's')]
session: Option<String>,
}
#[derive(Subcommand, Debug)]
enum Commands {
New,
Resume {
session_id: String,
},
Sessions,
Run {
prompt: String,
#[arg(long, short = 's')]
session: Option<String>,
},
Config,
}
fn create_default_registry(tech_query_url: &str) -> ToolRegistry {
let mut registry = ToolRegistry::new();
registry.register(Box::new(crate::tools::bash::BashTool::new()));
registry.register(Box::new(crate::tools::glob_search::GlobSearchTool::new()));
registry.register(Box::new(crate::tools::grep_search::GrepSearchTool::new()));
registry.register(Box::new(crate::tools::read_file::ReadFileTool::new()));
registry.register(Box::new(crate::tools::write_file::WriteFileTool::new()));
registry.register(Box::new(crate::tools::edit_file::EditFileTool::new()));
registry.register(Box::new(crate::tools::tech_query::TechQueryTool::new(Some(tech_query_url.to_string()))));
registry.register(Box::new(crate::tools::web_search::WebSearchTool::new()));
registry.register(Box::new(crate::tools::web_fetch::WebFetchTool::new()));
registry
}
fn get_os_info() -> (String, String) {
let os_name = std::env::consts::OS.to_string();
let os_version = if cfg!(target_os = "windows") {
if let Ok(output) = std::process::Command::new("cmd").args(["/c", "ver"]).output() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
"Windows".to_string()
}
} else if cfg!(target_os = "macos") {
if let Ok(output) = std::process::Command::new("sw_vers").arg("-productVersion").output() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
"macOS".to_string()
}
} else {
if let Ok(output) = std::process::Command::new("uname").arg("-r").output() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
"Linux/Unix".to_string()
}
};
(os_name, os_version)
}
async fn build_system_prompt_with_memory(session_dir: &std::path::Path, model_info: &str) -> String {
let mut memory_notes = Vec::new();
let memory_path = session_dir.join("memory.json");
if memory_path.exists() {
if let Ok(json) = std::fs::read_to_string(&memory_path) {
if let Ok(store) = serde_json::from_str::<serde_json::Value>(&json) {
if let Some(notes) = store["notes"].as_array() {
for note in notes {
if let Some(text) = note["text"].as_str() {
memory_notes.push(text.to_string());
}
}
}
}
}
}
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let (os_name, os_version) = get_os_info();
let project_context = crate::prompt::ProjectContext::discover(cwd);
crate::prompt::SystemPromptBuilder::new()
.with_os(os_name, os_version)
.with_model_info(model_info.to_string())
.with_project_context(project_context)
.with_memory_notes(memory_notes)
.build()
}
async fn resolve_session_id(session_dir: &std::path::Path, prefix: &str) -> Result<String> {
if prefix.len() == 36 {
return Ok(prefix.to_string());
}
let summaries = Session::list_all(session_dir).await?;
let matching: Vec<_> = summaries.into_iter().filter(|s| s.id.starts_with(prefix)).collect();
if matching.is_empty() {
anyhow::bail!("No session found matching prefix '{}'", prefix);
} else if matching.len() > 1 {
anyhow::bail!(
"Multiple sessions match prefix '{}':\n{}",
prefix,
matching.iter().map(|s| format!(" - [{}] {}", &s.id[..8], s.title)).collect::<Vec<_>>().join("\n")
);
}
Ok(matching[0].id.clone())
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let mut config = AppConfig::from_env();
let provider_name = cli.provider.unwrap_or_else(|| config.default_provider.clone());
let model_name = cli.model.clone();
config.prompt_for_key_if_missing(&provider_name)?;
match cli.command {
Some(Commands::Sessions) => {
let summaries = Session::list_all(&config.session_dir).await?;
if summaries.is_empty() {
println!("{}", "No saved sessions found.".dimmed());
} else {
println!("{}", "\n━━━ Saved Sessions ━━━".bold());
for summary in &summaries {
println!(" {}", summary);
}
}
return Ok(());
}
Some(Commands::Config) => {
println!("{}", config.display_summary());
return Ok(());
}
Some(Commands::Run { prompt, session }) => {
let provider = create_provider(&config, &provider_name, model_name)?;
let model_info = format!("{} ({})", provider.name(), provider.model_id());
let system_prompt = build_system_prompt_with_memory(&config.session_dir, &model_info).await;
let tools = create_default_registry(&config.tech_query_url);
let tool_defs = tools.definitions();
let session = match session {
Some(sid) => {
let full_id = resolve_session_id(&config.session_dir, &sid).await?;
Session::load(&config.session_dir, &full_id).await?
}
None => Session::new(provider.name(), provider.model_id()),
};
let engine = QueryEngine::new(provider, system_prompt, tool_defs).with_max_tokens(config.max_tokens);
let mut agent = Agent::new(engine, tools, session, config);
agent.run_turn(&prompt).await?;
return Ok(());
}
Some(Commands::Resume { session_id }) => {
let full_id = resolve_session_id(&config.session_dir, &session_id).await?;
run_repl(config, &provider_name, model_name, Some(full_id)).await?;
}
Some(Commands::New) | None => {
let mut session_id = cli.session;
if session_id.is_none() {
if let Ok(summaries) = Session::list_all(&config.session_dir).await {
if let Some(latest) = summaries.first() {
println!("{}", format!("Found recent session: {}", latest).cyan());
print!("Would you like to resume this session? (y/N): ");
use std::io::Write;
let _ = std::io::stdout().flush();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let trimmed = input.trim().to_lowercase();
if trimmed == "y" || trimmed == "yes" {
session_id = Some(latest.id.clone());
}
}
}
}
}
run_repl(config, &provider_name, model_name, session_id).await?;
}
}
Ok(())
}
async fn run_repl(
config: AppConfig,
provider_name: &str,
model_name: Option<String>,
session_id: Option<String>,
) -> Result<()> {
let provider = create_provider(&config, provider_name, model_name)?;
let model_info = format!("{} ({})", provider.name(), provider.model_id());
let system_prompt = build_system_prompt_with_memory(&config.session_dir, &model_info).await;
let tools = create_default_registry(&config.tech_query_url);
let tool_defs = tools.definitions();
let session = match session_id {
Some(sid) => {
let full_id = resolve_session_id(&config.session_dir, &sid).await?;
println!("{}", format!("Resuming session: {}", &full_id[..8]).green());
Session::load(&config.session_dir, &full_id).await?
}
None => {
let s = Session::new(provider.name(), provider.model_id());
println!("{}", format!("Starting new session: {}", &s.id[..8]).green());
s
}
};
let engine = QueryEngine::new(provider, system_prompt, tool_defs).with_max_tokens(config.max_tokens);
let mut agent = Agent::new(engine, tools, session, config.clone());
println!("{}", "\n━━━ Gigi AI Coding Assistant CLI ━━━".bold().cyan());
println!(" Model: {}", agent.model_info().bold());
println!(" Type {} for a list of commands, or ask any question.", "/help".cyan());
println!(" Press Ctrl+C or Ctrl+D to exit.\n");
let mut rl = DefaultEditor::new()?;
let history_path = config.session_dir.join("history.txt");
let _ = rl.load_history(&history_path);
loop {
let prompt = format!("{} ❯ ", "Gigi".cyan().bold());
let readline = rl.readline(&prompt);
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
let _ = rl.add_history_entry(line);
let _ = rl.save_history(&history_path);
if line == "/exit" || line == "/quit" {
println!("Goodbye!");
break;
}
let handled = crate::commands::dispatch(&mut agent, line).await;
match handled {
Ok(true) => {}
Ok(false) => {
if let Err(e) = agent.run_turn(line).await {
println!("{}", format!("Error: {}", e).red());
}
}
Err(e) => {
println!("{}", format!("Command error: {}", e).red());
}
}
}
Err(ReadlineError::Interrupted) => {
println!("Interrupted.");
break;
}
Err(ReadlineError::Eof) => {
println!("EOF.");
break;
}
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
Ok(())
}