feat: add version string to startup and status menu
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
387
Program.cs
387
Program.cs
@@ -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
Reference in New Issue
Block a user