com.IvanMurzak.ReflectorNet
5.1.1
dotnet add package com.IvanMurzak.ReflectorNet --version 5.1.1
NuGet\Install-Package com.IvanMurzak.ReflectorNet -Version 5.1.1
<PackageReference Include="com.IvanMurzak.ReflectorNet" Version="5.1.1" />
<PackageVersion Include="com.IvanMurzak.ReflectorNet" Version="5.1.1" />
<PackageReference Include="com.IvanMurzak.ReflectorNet" />
paket add com.IvanMurzak.ReflectorNet --version 5.1.1
#r "nuget: com.IvanMurzak.ReflectorNet, 5.1.1"
#:package com.IvanMurzak.ReflectorNet@5.1.1
#addin nuget:?package=com.IvanMurzak.ReflectorNet&version=5.1.1
#tool nuget:?package=com.IvanMurzak.ReflectorNet&version=5.1.1
ReflectorNet
ReflectorNet is a sophisticated .NET reflection toolkit designed to bridge the gap between static .NET applications and dynamic, AI-driven environments. It provides robust serialization, intelligent method discovery, and dynamic invocation capabilities that allow AI agents to interact with .NET codebases safely and effectively.
🚀 Why ReflectorNet?
Traditional reflection is brittle and requires exact matches. ReflectorNet is built for flexibility:
- 🤖 AI-Ready: Designed for scenarios where inputs (from LLMs) might be partial or fuzzy.
- 🔍 Fuzzy Matching: Discover methods and types even with incomplete names or parameters (configurable match levels 0-6).
- 📦 Type-Safe Serialization: Preserves full type information, supporting complex nested objects, collections, and custom types.
- 🔄 In-Place Modification: Update existing object instances from serialized data without breaking references.
- 🎯 Atomic Path-Based Modification: Navigate directly to any field, array element, or dictionary entry by path and modify only that — without touching anything else.
- 🩹 JSON Patch: Apply a JSON document to modify multiple fields at different depths in a single call, following JSON Merge Patch (RFC 7396) semantics.
- 🔎 View & Grep: Read exactly what you need — navigate to a subtree, filter by name pattern or type, or grep the entire live object graph for all fields matching a regex. The read-side counterpart to path-based modification.
- 📄 JSON Schema Generation: Automatically generate schemas for your types and methods to feed into LLM context windows.
📦 Installation
dotnet add package com.IvanMurzak.ReflectorNet
⚡ Quick Start
1. Setup
The Reflector class is your main entry point.
using com.IvanMurzak.ReflectorNet;
var reflector = new Reflector();
2. Serialization
Convert any .NET object into a SerializedMember intermediate representation. This preserves type metadata that standard JSON serializers might lose.
var myObject = new MyComplexClass { Id = 1, Name = "Test" };
// Serialize to intermediate representation
SerializedMember serialized = reflector.Serialize(myObject);
// Convert to JSON string if needed
string json = reflector.JsonSerializer.Serialize(serialized);
3. Deserialization
Reconstruct objects with full type fidelity.
// Restore to a specific type
MyComplexClass restored = reflector.Deserialize<MyComplexClass>(serialized);
// Or let Reflector resolve the type automatically
object restoredObj = reflector.Deserialize(serialized);
4. In-Place Modification
Update an existing object instance with new data. This is crucial for maintaining object identity in stateful applications (like Unity games or long-running services).
var existingInstance = new MyComplexClass();
// Modify 'existingInstance' with data from 'serialized'
// Returns true if successful
bool success = reflector.TryModify(ref existingInstance, serialized);
5. Atomic Path-Based Modification
Navigate directly to a specific field, array element, or dictionary entry by path and modify only that target — no surrounding data is affected.
Path format:
| Segment | Meaning |
|---|---|
fieldName |
Field or property by name |
[i] |
Array / list element at index i |
[key] |
Dictionary entry with key key (any key type) |
A leading #/ is stripped automatically for compatibility with SerializationContext paths.
var reflector = new Reflector();
object? system = new SolarSystem
{
globalOrbitSpeedMultiplier = 1f,
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 10f, orbitSpeed = 1f },
new CelestialBody { orbitRadius = 20f, orbitSpeed = 2f },
}
};
// Modify a root field
reflector.TryModifyAt<float>(ref system, "globalOrbitSpeedMultiplier", 5f);
// Modify a nested field — only that one field changes
reflector.TryModifyAt<float>(ref system, "celestialBodies/[0]/orbitRadius", 999f);
// Dictionary entry — string or integer keys both work
object? container = new Config { settings = new Dictionary<string, int> { ["timeout"] = 10 } };
reflector.TryModifyAt<int>(ref container, "settings/[timeout]", 60);
For partial updates of a complex object at a path, supply a SerializedMember that lists only the fields to change:
var patch = new SerializedMember { typeName = typeof(CelestialBody).GetTypeId() };
patch.SetFieldValue(reflector, "orbitRadius", 777f); // only orbitRadius changes
var logs = new Logs();
reflector.TryModifyAt(ref system, "celestialBodies/[1]", patch, logs: logs);
// celestialBodies[1].orbitSpeed is untouched
Errors are collected in the Logs object — nothing is thrown:
var logs = new Logs();
bool ok = reflector.TryModifyAt<float>(ref system, "doesNotExist", 5f, logs: logs);
// ok == false; logs contains:
// "Segment 'doesNotExist' not found on type 'SolarSystem'.
// Available fields: globalOrbitSpeedMultiplier, celestialBodies, ..."
6. JSON Patch
Apply a JSON document to modify multiple fields at different depths in a single call. Follows JSON Merge Patch (RFC 7396) semantics, extended with bracket-notation keys for arrays and dictionaries.
var reflector = new Reflector();
object? system = new SolarSystem { /* ... */ };
// Modify several fields at once — untouched fields are preserved
var logs = new Logs();
bool ok = reflector.TryPatch(ref system, """
{
"globalOrbitSpeedMultiplier": 5.0,
"globalSizeMultiplier": 2.0,
"celestialBodies": {
"[0]": { "orbitRadius": 42.0 }
}
}
""", logs: logs);
Patch document rules:
- A JSON object key navigates into that field (
"fieldName") or element ("[i]"/"[key]") - A JSON non-object value sets the field directly
nullsets the field tonull"$type"key inside a JSON object specifies a desired subtype — the existing instance is replaced with a fresh instance of the new type before applying the remaining keys
// Replace a base-type field with a derived type and set its fields
reflector.TryPatch(ref system, """
{
"star": {
"$type": "MyNamespace.NeutronStar",
"mass": 2.5
}
}
""");
A JsonElement overload is also available when you already have a parsed document:
using var doc = JsonDocument.Parse(@"{ ""globalOrbitSpeedMultiplier"": 9.0 }");
reflector.TryPatch(ref system, doc.RootElement, logs: logs);
7. View & Grep — Read-Side Navigation
Read exactly the data you need from a live object — navigate to a specific subtree, filter by name or type, or grep the entire graph for every field matching a pattern. This is the read-side counterpart to Atomic Path-Based Modification.
reflector.View — filtered serialization
Returns a SerializedMember tree with optional path navigation and post-filters applied.
var reflector = new Reflector();
object? system = new SolarSystem
{
globalOrbitSpeedMultiplier = 1f,
globalSizeMultiplier = 2f,
celestialBodies = new[]
{
new CelestialBody { orbitRadius = 10f, orbitSpeed = 1f },
new CelestialBody { orbitRadius = 20f, orbitSpeed = 2f },
}
};
// Full view — equivalent to Serialize()
SerializedMember? full = reflector.View(system);
// Navigate to a subtree (same path format as TryModifyAt)
SerializedMember? firstBody = reflector.View(system,
new ViewQuery { Path = "celestialBodies/[0]" });
// firstBody.typeName contains "CelestialBody"
// Depth-limited — MaxDepth=0 returns root node only (no children)
SerializedMember? shallow = reflector.View(system,
new ViewQuery { MaxDepth = 1 });
// Pattern filter — keep only branches containing a matching field name
// Accepts any .NET regex; matching is case-insensitive
SerializedMember? orbitFields = reflector.View(system,
new ViewQuery { NamePattern = "^orbit" });
// Type filter — keep only branches whose resolved type is assignable to float
SerializedMember? floatFields = reflector.View(system,
new ViewQuery { TypeFilter = typeof(float) });
// Combined — navigate first, then filter
SerializedMember? result = reflector.View(system, new ViewQuery
{
Path = "celestialBodies/[0]",
NamePattern = "^orbit",
MaxDepth = 2,
});
When a filter produces no matches the root envelope is still returned with an empty fields collection so that result.typeName always identifies the navigated node's type.
ViewQuery options:
| Option | Type | Default | Description |
|---|---|---|---|
Path |
string? |
null |
Navigate to this path before serializing (same format as TryModifyAt) |
MaxDepth |
int? |
null |
Maximum depth of returned tree (0 = root node only, no children) |
NamePattern |
string? |
null |
.NET regex matched against field / property names (case-insensitive) |
TypeFilter |
Type? |
null |
Keep only branches whose resolved runtime type is assignable to this type |
reflector.TryReadAt — single-value path read
Navigate to exactly one value by path and serialize it. Mirrors TryModifyAt but reads instead of writes.
// Scalar leaf
bool ok = reflector.TryReadAt(system, "celestialBodies/[0]/orbitRadius", out SerializedMember? r);
if (ok)
Console.WriteLine(r!.GetValue<float>(reflector)); // 10f
// Composite node — result has full fields tree for the navigated object
reflector.TryReadAt(system, "celestialBodies/[0]", out var body);
float radius = body!.fields!.First(f => f.name == "orbitRadius").GetValue<float>(reflector);
// Dictionary access — string or any key type
reflector.TryReadAt(container, "config/[timeout]", out var timeout);
int ms = timeout!.GetValue<int>(reflector); // 30
// Invalid paths return false; errors are collected in Logs — nothing is thrown
var logs = new Logs();
bool ok2 = reflector.TryReadAt(system, "doesNotExist", out _, logs: logs);
// ok2 == false
// logs: "Segment 'doesNotExist' not found on type 'SolarSystem'.
// Available fields: globalOrbitSpeedMultiplier, celestialBodies, ..."
reflector.Grep — grep the live object graph
Walks the entire live object graph and returns a flat list of every field / property whose name matches the given regex pattern — like the grep command, but for in-RAM objects.
// Find every field whose name starts with "orbit"
IReadOnlyList<ViewMatch> hits = reflector.Grep(system, "^orbit");
foreach (var hit in hits)
Console.WriteLine($"{hit.Path} = {hit.Value.GetValue<float>(reflector)}");
// celestialBodies/[0]/orbitRadius = 10
// celestialBodies/[0]/orbitSpeed = 1
// celestialBodies/[1]/orbitRadius = 20
// celestialBodies/[1]/orbitSpeed = 2
// Limit search depth (0 = top-level fields only, no recursion)
IReadOnlyList<ViewMatch> topLevel = reflector.Grep(system, ".*", maxDepth: 0);
// Exact name — anchored regex
IReadOnlyList<ViewMatch> exact = reflector.Grep(system, "^globalOrbitSpeedMultiplier$");
Console.WriteLine(exact[0].Path); // "globalOrbitSpeedMultiplier"
Each ViewMatch exposes:
Path— full slash-delimited path, e.g."celestialBodies/[0]/orbitRadius"Value—SerializedMemberof the matched field, ready forGetValue<T>(reflector)
Grep vs. View + NamePattern:
Grepwalks the live object graph and can find fields inside array elements.View+NamePatternfilters theSerializedMembertree after serialization; array element contents are stored as JSON and are not individually filterable by name. UseGrepwhen you need to search inside arrays.
8. Dynamic Method Invocation
Allow AI to find and call methods without knowing the exact signature.
using com.IvanMurzak.ReflectorNet.Model;
// 1. Define what we are looking for (can be partial)
var methodRef = new MethodRef
{
TypeName = "Calculator",
MethodName = "Add", // Could be "AddValues" or "CalculateAdd" depending on match level
InputParameters = new List<MethodRef.Parameter>
{
new MethodRef.Parameter { Name = "a", Value = "10" },
new MethodRef.Parameter { Name = "b", Value = "20" }
}
};
// 2. Call the method
// Note: We pass 'reflector' as the first argument to handle internal deserialization context
string result = reflector.MethodCall(
reflector,
methodRef,
methodNameMatchLevel: 3, // Allow fuzzy matching
executeInMainThread: false // Set to false for console apps/services (no UI thread)
);
Console.WriteLine(result); // Output: [Success] 30
9. Method Inspection & Schema Generation
Generate JSON schemas for types and methods to help LLMs understand your code structure.
// 1. Get schema for a specific type
var typeSchema = reflector.GetSchema<MyComplexClass>();
// 2. Get schema for method arguments (ideal for LLM function calling definitions)
var methodInfo = typeof(Calculator).GetMethod("Add");
var argsSchema = reflector.GetArgumentsSchema(methodInfo);
// 3. Get schema for method return value
var returnSchema = reflector.GetReturnSchema(methodInfo);
🏗️ Architecture
ReflectorNet is built on a Chain of Responsibility pattern to handle the complexity of .NET types.
Core Components
Reflector: The orchestrator. It manages the registry of converters and exposes the high-level API.Registry: Holds a prioritized list ofIReflectionConverters. When you serialize or deserialize, the registry finds the best converter for the specific type.SerializedMember: The universal data model. It represents any .NET object (primitive, class, array) in a serializable format that holds both value and type metadata.
Built-in Converters
ReflectorNet comes with a set of standard converters:
PrimitiveReflectionConverter: Handlesint,string,bool,DateTime, etc.ArrayReflectionConverter: Handles arrays (T[]) and generic lists (List<T>).GenericReflectionConverter<T>: The fallback for custom classes and structs.TypeReflectionConverter&AssemblyReflectionConverter: Specialized handling forSystem.TypeandSystem.Reflection.Assembly.
Extensibility
You can create custom converters for your own types by implementing IReflectionConverter or inheriting from GenericReflectionConverter<T> and registering them:
reflector.Converters.Add(new MyCustomConverter());
🛠️ Advanced Features
JSON Schema Generation
Generate schemas to describe your C# types to an LLM.
// Get schema for a type
var typeSchema = reflector.GetSchema<MyClass>();
// Get schema for method arguments (great for function calling)
var methodSchema = reflector.GetArgumentsSchema(myMethodInfo);
🧩 The Converter System (Custom Serialization)
ReflectorNet's power lies in its extensible Converter System. If you have "exotic" data models (e.g., third-party types you can't modify, complex graphs, or types needing special handling like System.Type), you can write a custom converter.
How it Works
- Interface: All converters implement
IReflectionConverter. - Base Class: Most custom converters should inherit from
BaseReflectionConverter<T>orGenericReflectionConverter<T>. - Priority: ReflectorNet asks every registered converter: "Can you handle this type, and how well?" (via
SerializationPriority). The one with the highest score wins.- Exact match: Highest priority.
- Inheritance match: Lower priority (based on distance).
- No match: Zero.
Creating a Custom Converter
Here is an example of a converter for a hypothetical ThirdPartyWidget that should be serialized as a simple string instead of a complex object.
<details> <summary>Click to see the code example</summary>
using com.IvanMurzak.ReflectorNet.Converter;
using com.IvanMurzak.ReflectorNet.Model;
// 1. Inherit from GenericReflectionConverter<T> for the target type
public class WidgetConverter : GenericReflectionConverter<ThirdPartyWidget>
{
// 2. Override SerializationPriority if you need special matching logic
// (The default implementation already handles inheritance distance perfectly)
// 3. Override InternalSerialize to customize output
protected override SerializedMember InternalSerialize(
Reflector reflector,
object? obj,
Type type,
string? name = null,
bool recursive = true,
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
int depth = 0,
Logs? logs = null,
ILogger? logger = null,
SerializationContext? context = null)
{
if (obj is ThirdPartyWidget widget)
{
// Serialize as a simple string value instead of an object with fields
return SerializedMember.FromValue(
reflector,
type,
value: $"Widget:{widget.Id}",
name: name
);
}
return base.InternalSerialize(reflector, obj, type, name, recursive, flags, depth, logs, logger, context);
}
// 4. Override CreateInstance if the type has no parameterless constructor
public override object? CreateInstance(Reflector reflector, Type type)
{
return new ThirdPartyWidget("default-id");
}
}
// 5. Register it
reflector.Converters.Add(new WidgetConverter());
</details>
Lazy Loading & Optional Dependencies
Sometimes you need to handle types that might not be present at runtime (e.g., optional plugins or platform-specific referencing like UnityEngine.Collider which is only available inside Unity). If you reference these types directly in your code, your application might crash if the assembly is missing.
LazyReflectionConverter solves this by resolving the type by its string name at runtime. If the type is found, it works; if not, it gracefully steps aside.
Basic Usage:
// Only active if "Some.Optional.Library.SuperWidget" exists at runtime
var lazyConverter = new LazyReflectionConverter("Some.Optional.Library.SuperWidget");
reflector.Converters.Add(lazyConverter);
Advanced Usage: Delegation & Ignoring Members
You can also use LazyReflectionConverter to wrap your own custom converters. This allows you to apply your custom logic only when the type exists, without taking a hard dependency on it.
// 1. Create your custom converter (assuming it can compile without the hard dependency, e.g. using generics or object)
// Or, if you have a converter that specific to a type but you want to lazy load it:
var myCustomLogic = new MySpecialConverter(); // Implements IReflectionConverter
// 2. Wrap it in LazyReflectionConverter
var lazyDelegate = new LazyReflectionConverter(
"UnityEngine.Collider",
backingConverter: myCustomLogic
);
reflector.Converters.Add(lazyDelegate);
You can also simply ignore specific properties or fields without writing a full custom converter:
// Ignore "heavyData" property if the type exists
var simpleLazy = new LazyReflectionConverter(
"My.Optional.Type",
ignoredProperties: new[] { "heavyData" }
);
📜 Custom JSON Schema Generation
While ReflectionConverter handles runtime object manipulation, you might also want to control how your types are described in the generated JSON Schema (used by LLMs to understand your data structure).
ReflectorNet allows you to customize this by implementing the IJsonSchemaConverter interface. This is often done by inheriting from JsonSchemaConverter<T>, which combines standard JSON serialization with schema generation.
How it Works
- Interface: Implement
IJsonSchemaConverter. - Registration: Add the converter to
reflector.JsonSerializer. - Generation: When
reflector.GetSchema()is called, it checks if a registered converter exists for a type. If that converter implementsIJsonSchemaConverter, it delegates schema creation to it.
Example: Custom Schema for a Widget
Suppose you have a ThirdPartyWidget that serializes to a string (e.g., "Widget:123"), and you want the LLM to know this format.
<details> <summary>Click to see the code example</summary>
using System.Text.Json;
using System.Text.Json.Nodes;
using com.IvanMurzak.ReflectorNet.Converter.Json;
using com.IvanMurzak.ReflectorNet.Utils;
// 1. Inherit from JsonSchemaConverter<T>
public class WidgetSchemaConverter : JsonSchemaConverter<ThirdPartyWidget>
{
// 2. Define the Schema Definition (what the type looks like)
public override JsonNode GetSchema()
{
return new JsonObject
{
[JsonSchema.Type] = "string",
[JsonSchema.Pattern] = "^Widget:\\d+$",
[JsonSchema.Description] = "A widget identifier in the format 'Widget:{id}'"
};
}
// 3. Define the Schema Reference (how other types refer to it)
public override JsonNode GetSchemaRef()
{
// Standard way to refer to the definition
return new JsonObject
{
[JsonSchema.Ref] = JsonSchema.RefValue + TypeUtils.GetSchemaTypeId<ThirdPartyWidget>()
};
}
// 4. Implement standard System.Text.Json logic (optional if only used for schema, but recommended)
public override void Write(Utf8JsonWriter writer, ThirdPartyWidget value, JsonSerializerOptions options)
{
writer.WriteStringValue($"Widget:{value.Id}");
}
public override ThirdPartyWidget Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
// Parse "Widget:123" back to object...
return new ThirdPartyWidget(str.Split(':')[1]);
}
}
// 5. Register it with the JSON Serializer
reflector.JsonSerializer.AddConverter(new WidgetSchemaConverter());
</details>
Note: You can use
ReflectionConverter(for runtime logic) andJsonSchemaConverter(for schema/transport) together for the same type if needed.
Fuzzy Matching Levels
When searching for methods, you can tune the strictness:
- 6: Exact match
- 5: Case-insensitive match
- 4: Starts with (Case-sensitive)
- 3: Starts with (Case-insensitive)
- 2: Contains (Case-sensitive)
- 1: Contains (Case-insensitive)
🤝 Contributing
Contributions are welcome! Please submit Pull Requests to the main branch.
- Fork the repository.
- Create a feature branch.
- Commit your changes.
- Push to the branch.
- Open a Pull Request.
📄 License
This project is licensed under the Apache-2.0 License. Copyright - Ivan Murzak.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Microsoft.Extensions.Logging (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Extensions.Logging (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
-
net9.0
- Microsoft.Extensions.Logging (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on com.IvanMurzak.ReflectorNet:
| Package | Downloads |
|---|---|
|
com.IvanMurzak.McpPlugin.Common
McpPlugin common code for McpPlugin and McpPlugin.Server projects. It is .NET Library project for integration MCP server features into any dotnet application. It connects automatically with MCP server and exposes it's API over TCP connection in runtime. When MCP server interacts with AI. |
|
|
com.IvanMurzak.McpPlugin
McpPlugin is a .NET Library project for integration MCP server features into any dotnet application. It connects automatically with MCP server and exposes it's API over TCP connection in runtime. When MCP server interacts with AI. This library maintains a local application. |
|
|
com.IvanMurzak.McpPlugin.Server
MCP Server dotnet. Model Context Protocol server that interacts with MCP Plugin integrated into any dotnet application. |
|
|
com.IvanMurzak.Unity.MCP.Common
Shared code between Unity-MCP-Plugin and Unity-MCP-Server projects. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on com.IvanMurzak.ReflectorNet:
| Repository | Stars |
|---|---|
|
IvanMurzak/Unity-MCP
AI Skills, MCP Tools, and CLI for Unity Engine. Full AI develop and test loop. Use cli for quick setup. Efficient token usage, advanced tools. Any C# method may be turned into a tool by a single line. Works with Claude Code, Gemini, Copilot, Cursor and any other absolutely for free.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 5.1.1 | 1,088 | 4/29/2026 |
| 5.1.0 | 164 | 4/29/2026 |
| 5.0.0 | 5,659 | 4/16/2026 |
| 4.1.0 | 792 | 3/25/2026 |
| 4.0.0 | 2,081 | 3/3/2026 |
| 3.12.1 | 1,207 | 2/12/2026 |
| 3.12.0 | 459 | 1/31/2026 |
| 3.11.0 | 372 | 1/20/2026 |
| 3.10.0 | 301 | 1/19/2026 |
| 3.9.0 | 282 | 1/18/2026 |
| 3.8.1 | 265 | 1/18/2026 |
| 3.8.0 | 272 | 1/18/2026 |
| 3.7.1 | 318 | 1/17/2026 |
| 3.7.0 | 279 | 1/16/2026 |
| 3.6.0 | 338 | 1/10/2026 |
| 3.5.0 | 330 | 1/10/2026 |
| 3.4.0 | 286 | 1/8/2026 |
| 3.3.1 | 324 | 1/7/2026 |
| 3.3.0 | 321 | 1/5/2026 |
| 3.2.4 | 288 | 1/4/2026 |