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 System.Linq;
using System.Reflection;
namespace AnchorCli.Commands;
public class HelpCommand : ICommand
@@ -16,6 +16,8 @@ public class HelpCommand : ICommand
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:[/]");
var table = new Table();

View File

@@ -1,4 +1,5 @@
using Spectre.Console;
using System.Reflection;
namespace AnchorCli.Commands;
public class StatusCommand : ICommand
@@ -25,6 +26,7 @@ public class StatusCommand : ICommand
table.AddRow("[grey]Model[/]", $"[cyan]{Markup.Escape(_model)}[/]");
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)}[/]");
AnsiConsole.Write(table);

View File

@@ -1,189 +1,198 @@
using System.ClientModel;
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);
var tokenTracker = new TokenTracker { Provider = tokenExtractor.ProviderName };
// ── Fetch model pricing (only for supported providers) ─────────────────
ModelInfo? modelInfo = 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);
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);
}
}
catch { /* pricing is best-effort */ }
});
}
// ── Pretty header ───────────────────────────────────────────────────────
AnsiConsole.Write(
new FigletText("anchor")
.Color(Color.CornflowerBlue));
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]CWD[/]", $"[green]{Markup.Escape(Environment.CurrentDirectory)}[/]");
if (modelInfo?.Pricing != null)
if (modelInfo?.Pricing != 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);
if (modelInfo != null)
{
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));
commandRegistry.Register(new SaveCommand(session));
commandRegistry.Register(new LoadCommand(session));
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 */ }
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);
var tokenTracker = new TokenTracker { Provider = tokenExtractor.ProviderName };
// ── Fetch model pricing (only for supported providers) ─────────────────
ModelInfo? modelInfo = 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);
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);
}
}
catch { /* pricing is best-effort */ }
});
}
// ── Pretty header ───────────────────────────────────────────────────────
AnsiConsole.Write(
new FigletText("anchor")
.Color(Color.CornflowerBlue));
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
AnsiConsole.Write(
new Rule("[dim]AI-powered coding assistant[/]")
.RuleStyle(Style.Parse("cornflowerblue dim"))
.LeftJustified());
// ── 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)
{
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);
if (modelInfo != null)
{
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));
commandRegistry.Register(new SaveCommand(session));
commandRegistry.Register(new LoadCommand(session));
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 */ }

File diff suppressed because it is too large Load Diff