From aeb4fe9dc00a272ea20bdd23e540c0960561ca84 Mon Sep 17 00:00:00 2001 From: psipher Date: Sun, 22 Mar 2026 16:32:27 +0100 Subject: [PATCH] feat: Interactive CLI Installer module and v0.3.0 UX upgrade --- Cargo.lock | 2 +- Cargo.toml | 2 +- config.toml | 3 + integrations/google_antigravity/GEMINI.md | 1 + src/installer.rs | 130 ++++++++++++++++++++++ src/main.rs | 30 +++-- 6 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/installer.rs diff --git a/Cargo.lock b/Cargo.lock index ea387c7..caeeb24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -563,7 +563,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lm-bridge" -version = "0.2.0" +version = "0.3.0" dependencies = [ "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index 65734ba..81ef42b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lm-bridge" -version = "0.2.1" +version = "0.3.0" edition = "2021" description = "MCP server connecting Google Antigravity to local LLMs via LM Studio" diff --git a/config.toml b/config.toml index 56c072f..90e4816 100644 --- a/config.toml +++ b/config.toml @@ -31,6 +31,8 @@ Requirements: - Stay within the existing repository stack and patterns implied by the context. - Do not switch frameworks, libraries, routing systems, build tools, or UI stacks unless the task explicitly requires it. - Do not invent unrelated scaffolding or sample app structure. +- Ensure all module imports are strictly necessary and remove unused dependencies. +- Handle macro evaluations natively; do not print raw macros (e.g., `include_str!`) as string literals unless explicitly requested. - If the task is for an existing file, generate code that fits that file's current role and surrounding code. Provide only the necessary code, ensuring it is correct, clean, and fulfills the task constraints. @@ -53,6 +55,7 @@ Requirements: - Make only the requested change. - Preserve the existing framework, stack, and file purpose. - Do not rewrite the file into a different framework or architecture. +- Ensure all module imports are strictly necessary and prune removed dependencies. - Do not add unrelated cleanup, refactors, formatting-only changes, or commentary. - Return only the modified code. diff --git a/integrations/google_antigravity/GEMINI.md b/integrations/google_antigravity/GEMINI.md index c1e7cc8..8483f99 100644 --- a/integrations/google_antigravity/GEMINI.md +++ b/integrations/google_antigravity/GEMINI.md @@ -5,3 +5,4 @@ Whenever you are asked to generate or edit source code, you should prioritize de * Always briefly explain your architectural plan before invoking the MCP tools to write the code. * Do not apologize or use filler phrases; keep your responses concise and technical. * When debugging, state your hypothesis clearly before editing files. +* When using `local_llm` tools to generate entirely new modules, explicitly map out the surrounding variables, borrowed scopes, and module dependencies in the tool's `Context` parameter to ensure the generated code integrates seamlessly into the broader AST without lifetime mismatching. diff --git a/src/installer.rs b/src/installer.rs new file mode 100644 index 0000000..23f82da --- /dev/null +++ b/src/installer.rs @@ -0,0 +1,130 @@ +// src/installer.rs + +use std::env; +use std::fs; +use std::io::{self, Write}; +use std::path::PathBuf; + +use serde_json::{json, Value}; + +/// Interactive installer for the Gemini integration. +pub async fn run_interactive_installer( + exe_path: String, + model: String, +) -> Result<(), Box> { + // Resolve home directory and gemini paths + #[cfg(windows)] + let home_dir = env::var("USERPROFILE").unwrap_or_else(|_| "C:\\".to_string()); + #[cfg(not(windows))] + let home_dir = env::var("HOME").unwrap_or_else(|_| "/".to_string()); + + let gemini_dir = PathBuf::from(home_dir).join(".gemini"); + let mcp_config_path = gemini_dir.join("antigravity/mcp_config.json"); + let gemini_md_path = gemini_dir.join("GEMINI.md"); + + // Helper to read a line from stdin + fn read_line(prompt: &str) -> io::Result { + print!("{}", prompt); + io::stdout().flush()?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) + } + + println!("Select an option:"); + println!("[1] Setup Manually (Generate files locally)"); + println!("[2] Auto-Install for Google Antigravity"); + let choice = read_line("Enter your choice: ")?; + + match choice.as_str() { + "1" => { + println!("\n✅ Successfully generated mcp_registration.json and config.toml in this directory."); + println!("MCP Setup is complete! Please attach this executable to your AI client."); + return Ok(()); // Let main.rs handle local write + } + "2" => { + if !gemini_dir.exists() { + println!("Unable to find Antigravity configuration folder at {}. Falling back to manual mode.", gemini_dir.display()); + return Ok(()); + } + + println!("\nWhat would you like to install?"); + println!("[1] MCP Config Only"); + println!("[2] Agent Rules Only"); + println!("[3] Both"); + let sub_choice = read_line("Enter your choice: ")?; + + println!( + "\nThe following files will be modified:\n- {}\n- {}", + mcp_config_path.display(), + gemini_md_path.display() + ); + println!("Note: Existing agent rules may be commented out."); + let confirm = read_line("Proceed? (y/n): ")?; + if !confirm.eq_ignore_ascii_case("y") { + println!("Installation aborted."); + return Ok(()); + } + + // MCP Config + if sub_choice == "1" || sub_choice == "3" { + let mut config: Value = if mcp_config_path.exists() { + let data = fs::read_to_string(&mcp_config_path)?; + match serde_json::from_str(&data) { + Ok(v) => v, + Err(e) => { + eprintln!("Failed to parse existing MCP config: {}. Please fix it manually. Aborting auto-install.", e); + return Ok(()); + } + } + } else { + json!({"mcpServers": {}}) + }; + + let local_node = json!({ + "command": exe_path, + "args": [], + "env": { "LM_STUDIO_MODEL": model } + }); + + if let Some(obj) = config.as_object_mut() { + if !obj.contains_key("mcpServers") { + obj.insert("mcpServers".to_string(), json!({})); + } + if let Some(servers) = obj.get_mut("mcpServers").and_then(|v| v.as_object_mut()) + { + servers.insert("local_llm".to_string(), local_node); + let new_json = serde_json::to_string_pretty(&config)?; + fs::write(&mcp_config_path, new_json)?; + println!("Updated MCP config at {}", mcp_config_path.display()); + } + } + } + + // GEMINI.md + if sub_choice == "2" || sub_choice == "3" { + let mut content = String::new(); + if gemini_md_path.exists() { + content = fs::read_to_string(&gemini_md_path)?; + } + + // Append the integration snippet + let include_snippet = include_str!("../integrations/google_antigravity/GEMINI.md"); + if !content.contains("Privacy & Tool Routing") { + content.push_str("\n\n"); + content.push_str(include_snippet); + fs::write(&gemini_md_path, content)?; + println!("Updated GEMINI.md at {}", gemini_md_path.display()); + } else { + println!("Local LLM rules already exist in GEMINI.md. Skipping."); + } + } + } + _ => { + eprintln!("Invalid choice. Exiting."); + return Ok(()); + } + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 102dd73..a1dfc50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,12 @@ use reqwest::ClientBuilder; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::env; +use std::io::IsTerminal; use std::time::Duration; use tokio::io::{AsyncBufReadExt, BufReader}; +mod installer; + #[derive(Deserialize, Debug, Clone)] struct Config { #[serde(default = "default_url")] @@ -82,9 +85,14 @@ async fn check_for_updates() { .user_agent("lm-bridge-updater") .timeout(Duration::from_secs(4)) .build(); - let Ok(client) = client else { return; }; - - let resp = client.get("https://api.github.com/repos/psipher/lm-bridge/releases/latest").send().await; + let Ok(client) = client else { + return; + }; + + let resp = client + .get("https://api.github.com/repos/psipher/lm-bridge/releases/latest") + .send() + .await; if let Ok(resp) = resp { if let Ok(json) = resp.json::().await { if let Some(tag_name) = json.get("tag_name").and_then(|v| v.as_str()) { @@ -182,18 +190,24 @@ async fn main() -> Result<(), Box> { // Check for interactive (double-click) mode or --register flag let args: Vec = env::args().collect(); - use std::io::IsTerminal; let is_interactive = std::io::stdin().is_terminal(); if args.contains(&"--register".to_string()) || is_interactive { - println!("\n✅ Successfully generated mcp_registration.json and config.toml in this directory."); if is_interactive { - println!("MCP Setup is complete! Please attach this executable to your AI client."); + let exe_path = env::current_exe()?.to_string_lossy().to_string(); + let result = installer::run_interactive_installer(exe_path, config.model.clone()).await; + if let Err(e) = result { + eprintln!("Installer error: {}", e); + } + // Ask user to press enter to close window println!("\nPress Enter to close this window..."); let mut buf = String::new(); let _ = std::io::stdin().read_line(&mut buf); + } else { + // CLI raw --register call + println!("\n✅ Successfully generated mcp_registration.json and config.toml in this directory."); } - return Ok(()); + std::process::exit(0); } let client = ClientBuilder::new() @@ -245,7 +259,7 @@ async fn main() -> Result<(), Box> { }, "serverInfo": { "name": "local_llm", - "version": "0.2.1" + "version": "0.3.0" } })), error: None,