1
0

feat: add version string to startup and status menu

This commit is contained in:
2026-03-06 08:13:39 +01:00
parent f687360c2b
commit 91a44bb2a4
4 changed files with 203 additions and 1291 deletions

View File

@@ -1,5 +1,5 @@
using Spectre.Console; using Spectre.Console;
using System.Linq; using System.Reflection;
namespace AnchorCli.Commands; namespace AnchorCli.Commands;
public class HelpCommand : ICommand public class HelpCommand : ICommand
@@ -16,6 +16,8 @@ public class HelpCommand : ICommand
public Task ExecuteAsync(string[] args, CancellationToken ct) public Task ExecuteAsync(string[] args, CancellationToken ct)
{ {
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
AnsiConsole.MarkupLine($"[cyan]Anchor CLI v{version}[/]");
AnsiConsole.MarkupLine("[cyan]Available commands:[/]"); AnsiConsole.MarkupLine("[cyan]Available commands:[/]");
var table = new Table(); var table = new Table();

View File

@@ -1,4 +1,5 @@
using Spectre.Console; using Spectre.Console;
using System.Reflection;
namespace AnchorCli.Commands; namespace AnchorCli.Commands;
public class StatusCommand : ICommand public class StatusCommand : ICommand
@@ -25,6 +26,7 @@ public class StatusCommand : ICommand
table.AddRow("[grey]Model[/]", $"[cyan]{Markup.Escape(_model)}[/]"); table.AddRow("[grey]Model[/]", $"[cyan]{Markup.Escape(_model)}[/]");
table.AddRow("[grey]Endpoint[/]", $"[blue]{Markup.Escape(_endpoint)}[/]"); table.AddRow("[grey]Endpoint[/]", $"[blue]{Markup.Escape(_endpoint)}[/]");
table.AddRow("[grey]Version[/]", $"[magenta]{Assembly.GetExecutingAssembly().GetName().Version}[/]");
table.AddRow("[grey]CWD[/]", $"[green]{Markup.Escape(Environment.CurrentDirectory)}[/]"); table.AddRow("[grey]CWD[/]", $"[green]{Markup.Escape(Environment.CurrentDirectory)}[/]");
AnsiConsole.Write(table); AnsiConsole.Write(table);

View File

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

File diff suppressed because it is too large Load Diff