using System.ClientModel; using System.Reflection; using AnchorCli.Providers; using Microsoft.Extensions.AI; using OpenAI; using AnchorCli; using AnchorCli.Tools; using AnchorCli.Commands; using AnchorCli.OpenRouter; using Spectre.Console; // ── Setup subcommand ───────────────────────────────────────────────────── if (args.Length > 0 && args[0].Equals("setup", StringComparison.OrdinalIgnoreCase)) { SetupTui.Run(); return; } // ── Config ────────────────────────────────────────────────────────────── var cfg = AnchorConfig.Load(); string apiKey = cfg.ApiKey; string model = cfg.Model; string provider = cfg.Provider ?? "openrouter"; string endpoint = cfg.Endpoint ?? "https://openrouter.ai/api/v1"; if (string.IsNullOrWhiteSpace(apiKey)) { AnsiConsole.MarkupLine("[red]No API key configured. Run [bold]anchor setup[/] first.[/]"); return; } // ── Create token extractor for this provider ─────────────────────────── var tokenExtractor = ProviderFactory.CreateTokenExtractorForEndpoint(endpoint); // ── Fetch model pricing (only for supported providers) ───────────────── ModelInfo? modelInfo = null; TokenTracker? tokenTracker = null; if (ProviderFactory.IsOpenRouter(endpoint)) { await AnsiConsole.Status() .Spinner(Spinner.Known.BouncingBar) .SpinnerStyle(Style.Parse("cornflowerblue")) .StartAsync("Fetching model pricing...", async ctx => { try { var pricingProvider = new OpenRouterProvider(); modelInfo = await pricingProvider.GetModelInfoAsync(model); } catch { /* pricing is best-effort */ } }); } // ── Pretty header ─────────────────────────────────────────────────────── AnsiConsole.Write( new FigletText("anchor") .Color(Color.CornflowerBlue)); var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; // ── Pretty header ─────────────────────────────────────────────────────── AnsiConsole.Write( new Rule("[dim]AI-powered coding assistant[/]") .RuleStyle(Style.Parse("cornflowerblue dim")) .LeftJustified()); AnsiConsole.WriteLine(); var infoTable = new Table() .Border(TableBorder.Rounded) .BorderColor(Color.Grey) .AddColumn(new TableColumn("[dim]Setting[/]").NoWrap()) .AddColumn(new TableColumn("[dim]Value[/]")); infoTable.AddRow("[grey]Model[/]", $"[cyan]{Markup.Escape(modelInfo?.Name ?? model)}[/]"); infoTable.AddRow("[grey]Provider[/]", $"[blue]{tokenExtractor.ProviderName}[/]"); infoTable.AddRow("[grey]Endpoint[/]", $"[dim]{endpoint}[/]"); infoTable.AddRow("[grey]Version[/]", $"[magenta]{version}[/]"); if (modelInfo?.Pricing != null && tokenTracker != null) { var inM = tokenTracker.InputPrice * 1_000_000m; var outM = tokenTracker.OutputPrice * 1_000_000m; infoTable.AddRow("[grey]Pricing[/]", $"[yellow]${inM:F2}[/][dim]/M in[/] [yellow]${outM:F2}[/][dim]/M out[/]"); } if (modelInfo != null) { infoTable.AddRow("[grey]Context[/]", $"[dim]{modelInfo.ContextLength:N0} tokens[/]"); } AnsiConsole.Write(infoTable); AnsiConsole.WriteLine(); // ── Build the chat client with tool-calling support ───────────────────── var httpClient = new HttpClient(); OpenRouterHeaders.ApplyTo(httpClient); var openAiClient = new OpenAIClient(new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint = new Uri(endpoint), Transport = new System.ClientModel.Primitives.HttpClientPipelineTransport(httpClient) }); IChatClient innerClient = openAiClient.GetChatClient(model).AsIChatClient(); // ── Tool call logging via Spectre ─────────────────────────────────────── object consoleLock = new(); void ToolLog(string message) { lock (consoleLock) { Console.Write("\r" + new string(' ', 40) + "\r"); AnsiConsole.MarkupLine($"[dim grey] ● {Markup.Escape(message)}[/]"); } } CommandTool.Log = DirTools.Log = FileTools.Log = EditTools.Log = ToolLog; // ── Instantiate Core Components ────────────────────────────────────────── var session = new ChatSession(innerClient); tokenTracker = new TokenTracker(session) { Provider = tokenExtractor.ProviderName }; if (modelInfo != null) { if (modelInfo.Pricing != null) { tokenTracker.InputPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Prompt); tokenTracker.OutputPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Completion); tokenTracker.RequestPrice = PricingProvider.ParsePrice(modelInfo.Pricing.Request); } tokenTracker.ContextLength = modelInfo.ContextLength; } var commandRegistry = new CommandRegistry(); commandRegistry.Register(new ExitCommand()); commandRegistry.Register(new HelpCommand(commandRegistry)); commandRegistry.Register(new ClearCommand()); commandRegistry.Register(new StatusCommand(model, endpoint)); commandRegistry.Register(new CompactCommand(session.Compactor, session.History)); commandRegistry.Register(new SetupCommand()); commandRegistry.Register(new ResetCommand(session, tokenTracker)); var commandDispatcher = new CommandDispatcher(commandRegistry); // ── Run Repl ──────────────────────────────────────────────────────────── // Auto-load session if it exists const string sessionPath = ".anchor/session.json"; if (File.Exists(sessionPath)) { try { await session.LoadAsync(sessionPath, default); AnsiConsole.MarkupLine($"[dim grey]Auto-loaded previous session.[/]"); // Print the last message if there is one if (session.History.Count > 1) { var lastMessage = session.History[^1]; var preview = lastMessage.Text.Length > 280 ? lastMessage.Text[..277] + "..." : lastMessage.Text; AnsiConsole.MarkupLine($"[dim grey] Last message: {Markup.Escape(preview)}[/]"); } } catch { /* Ignore load errors on startup */ } } var repl = new ReplLoop(session, tokenTracker, commandDispatcher); await repl.RunAsync(); // Auto-save session on clean exit try { var directory = Path.GetDirectoryName(sessionPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } await session.SaveAsync(sessionPath, default); } catch { /* Ignore save errors on exit */ }