diff --git a/AppJsonContext.cs b/AppJsonContext.cs
index d3727b7..c6e83c3 100644
--- a/AppJsonContext.cs
+++ b/AppJsonContext.cs
@@ -1,28 +1,29 @@
-using System.Text.Json.Serialization;
-using AnchorCli.OpenRouter;
-using AnchorCli.Tools;
-
-namespace AnchorCli;
-
-///
-/// Source-generated JSON serializer context for Native AOT compatibility.
-/// Covers all parameter / return types used by AIFunction tool methods
-/// and the OpenRouter models API.
-///
-[JsonSerializable(typeof(string))]
-[JsonSerializable(typeof(string[]))]
-[JsonSerializable(typeof(int))]
-[JsonSerializable(typeof(bool))]
-[JsonSerializable(typeof(string[][]))]
-[JsonSerializable(typeof(ModelsResponse))]
-[JsonSerializable(typeof(ModelInfo))]
-[JsonSerializable(typeof(ModelPricing))]
-[JsonSerializable(typeof(Microsoft.Extensions.AI.ChatMessage))]
-[JsonSerializable(typeof(System.Collections.Generic.List))]
-[JsonSerializable(typeof(AnchorConfig))]
-[JsonSerializable(typeof(BatchOperation))]
-[JsonSerializable(typeof(BatchOperation[]))]
-[JsonSourceGenerationOptions(
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
-internal partial class AppJsonContext : JsonSerializerContext;
+using System.Text.Json.Serialization;
+using AnchorCli.OpenRouter;
+using AnchorCli.Tools;
+
+namespace AnchorCli;
+
+///
+/// Source-generated JSON serializer context for Native AOT compatibility.
+/// Covers all parameter / return types used by AIFunction tool methods
+/// and the OpenRouter models API.
+///
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(string[]))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(string[][]))]
+[JsonSerializable(typeof(ModelsResponse))]
+[JsonSerializable(typeof(ModelInfo))]
+[JsonSerializable(typeof(ModelPricing))]
+[JsonSerializable(typeof(Microsoft.Extensions.AI.ChatMessage))]
+[JsonSerializable(typeof(System.Collections.Generic.List))]
+[JsonSerializable(typeof(AnchorConfig))]
+[JsonSerializable(typeof(BatchOperation))]
+[JsonSerializable(typeof(BatchOperation[]))]
+[JsonSerializable(typeof(TokenMetadata))]
+[JsonSourceGenerationOptions(
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+internal partial class AppJsonContext : JsonSerializerContext;
diff --git a/ChatSession.cs b/ChatSession.cs
index 9d3e518..8626ef5 100644
--- a/ChatSession.cs
+++ b/ChatSession.cs
@@ -8,6 +8,11 @@ internal sealed class ChatSession
private readonly IChatClient _agent;
public ContextCompactor Compactor { get; }
public List History { get; }
+
+ // Token tracking state persisted across sessions
+ public long SessionInputTokens { get; set; }
+ public long SessionOutputTokens { get; set; }
+ public int RequestCount { get; set; }
public ChatSession(IChatClient innerClient)
{
@@ -84,6 +89,18 @@ internal sealed class ChatSession
};
var json = JsonSerializer.Serialize(messagesToSave, AppJsonContext.Default.ListChatMessage);
+
+ // Save token stats to a separate metadata file
+ var metadataPath = Path.ChangeExtension(filePath, ".metadata.json");
+ var metadata = new TokenMetadata
+ {
+ SessionInputTokens = SessionInputTokens,
+ SessionOutputTokens = SessionOutputTokens,
+ RequestCount = RequestCount
+ };
+ var metadataJson = JsonSerializer.Serialize(metadata, AppJsonContext.Default.TokenMetadata);
+ await File.WriteAllTextAsync(metadataPath, metadataJson, cancellationToken);
+
await File.WriteAllTextAsync(filePath, json, cancellationToken);
}
@@ -104,5 +121,33 @@ internal sealed class ChatSession
History.Clear();
History.Add(systemMessage);
History.AddRange(messages);
+
+ // Load token stats from metadata file if it exists
+ var metadataPath = Path.ChangeExtension(filePath, ".metadata.json");
+ if (File.Exists(metadataPath))
+ {
+ try
+ {
+ var metadataJson = await File.ReadAllTextAsync(metadataPath, cancellationToken);
+ var metadata = JsonSerializer.Deserialize(metadataJson, AppJsonContext.Default.TokenMetadata);
+ if (metadata != null)
+ {
+ SessionInputTokens = metadata.SessionInputTokens;
+ SessionOutputTokens = metadata.SessionOutputTokens;
+ RequestCount = metadata.RequestCount;
+ }
+ }
+ catch { /* Ignore metadata load errors */ }
+ }
}
}
+
+///
+/// Token tracking metadata serialized with the session.
+///
+internal sealed class TokenMetadata
+{
+ public long SessionInputTokens { get; set; }
+ public long SessionOutputTokens { get; set; }
+ public int RequestCount { get; set; }
+}
diff --git a/Commands/LoadCommand.cs b/Commands/LoadCommand.cs
deleted file mode 100644
index 2375a88..0000000
--- a/Commands/LoadCommand.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Spectre.Console;
-
-namespace AnchorCli.Commands;
-
-internal class LoadCommand(ChatSession session) : ICommand
-{
- public string Name => "load";
- public string Description => "Load a chat session from a file";
-
- public async Task ExecuteAsync(string[] args, CancellationToken ct)
- {
- string filePath = args.Length > 0 ? args[0] : ".anchor/session.json";
-
- if (!File.Exists(filePath))
- {
- AnsiConsole.MarkupLine($"[yellow]No session file found at {Markup.Escape(filePath)}[/]");
- return;
- }
-
- try
- {
- await session.LoadAsync(filePath, ct);
- AnsiConsole.MarkupLine($"[green]Session loaded from {Markup.Escape(filePath)}[/]");
- }
- catch (Exception ex)
- {
- AnsiConsole.MarkupLine($"[red]Failed to load session: {Markup.Escape(ex.Message)}[/]");
- }
- }
-}
-
diff --git a/Commands/SaveCommand.cs b/Commands/SaveCommand.cs
deleted file mode 100644
index e94e61e..0000000
--- a/Commands/SaveCommand.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Spectre.Console;
-
-namespace AnchorCli.Commands;
-
-internal class SaveCommand(ChatSession session) : ICommand
-{
- public string Name => "save";
- public string Description => "Save the current chat session to a file";
-
- public async Task ExecuteAsync(string[] args, CancellationToken ct)
- {
- string filePath = args.Length > 0 ? args[0] : ".anchor/session.json";
-
- try
- {
- var directory = Path.GetDirectoryName(filePath);
- if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
- {
- Directory.CreateDirectory(directory);
- }
-
- await session.SaveAsync(filePath, ct);
- AnsiConsole.MarkupLine($"[green]Session saved to {Markup.Escape(filePath)}[/]");
- }
- catch (Exception ex)
- {
- AnsiConsole.MarkupLine($"[red]Failed to save session: {Markup.Escape(ex.Message)}[/]");
- }
- }
-}
-
diff --git a/OpenRouter/TokenTracker.cs b/OpenRouter/TokenTracker.cs
index 60caa62..e29d102 100644
--- a/OpenRouter/TokenTracker.cs
+++ b/OpenRouter/TokenTracker.cs
@@ -5,10 +5,17 @@ namespace AnchorCli.OpenRouter;
///
internal sealed class TokenTracker
{
+ private readonly ChatSession _session;
+
+ public TokenTracker(ChatSession session)
+ {
+ _session = session;
+ }
+
public string Provider { get; set; } = "Unknown";
- public long SessionInputTokens { get; private set; }
- public long SessionOutputTokens { get; private set; }
- public int RequestCount { get; private set; }
+ public long SessionInputTokens => _session.SessionInputTokens;
+ public long SessionOutputTokens => _session.SessionOutputTokens;
+ public int RequestCount => _session.RequestCount;
/// Maximum context window for the model (tokens). 0 = unknown.
public int ContextLength { get; set; }
@@ -29,16 +36,16 @@ internal sealed class TokenTracker
///
public void AddUsage(int inputTokens, int outputTokens)
{
- SessionInputTokens += inputTokens;
- SessionOutputTokens += outputTokens;
+ _session.SessionInputTokens += inputTokens;
+ _session.SessionOutputTokens += outputTokens;
LastInputTokens = inputTokens;
- RequestCount++;
+ _session.RequestCount++;
}
public void Reset()
{
- SessionInputTokens = 0;
- SessionOutputTokens = 0;
- RequestCount = 0;
+ _session.SessionInputTokens = 0;
+ _session.SessionOutputTokens = 0;
+ _session.RequestCount = 0;
LastInputTokens = 0;
}
diff --git a/Program.cs b/Program.cs
index 9c22345..b579c72 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,198 +1,193 @@
-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 */ }
-
+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 */ }
+
diff --git a/README.md b/README.md
index e7c552e..2dd455f 100644
--- a/README.md
+++ b/README.md
@@ -131,8 +131,6 @@ AnchorCli/
│ ├── CompactCommand.cs # /compact command
│ ├── ResetCommand.cs # /reset command
│ ├── SetupCommand.cs # /setup command
-│ ├── LoadCommand.cs # /load command
-│ └── SaveCommand.cs # /save command
└── OpenRouter/
└── PricingProvider.cs # Fetch model pricing from OpenRouter
```