Initial commit: ClawdDotNet

Import des bestehenden Projektstands in Git.
- .NET 10 WinForms Anwendung (Multi-Agent / Tool-System)
- .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-26 18:21:46 +02:00
co-authored by Claude Opus 4.8
commit 2fed388c99
154 changed files with 29736 additions and 0 deletions
@@ -0,0 +1,118 @@
using System.Collections.Concurrent;
using System.Text;
namespace ClawdDotNet.Core.Logging;
/// <summary>
/// Schreibt Log-Einträge asynchron in datei- und datumsgetrennte Logfiles.
/// Struktur: {LogDirectory}/{Datum}/{Modul}.log
/// Thread-safe durch ConcurrentQueue + dediziertem Writer-Task.
/// </summary>
public sealed class FileLogWriter : IAsyncDisposable
{
private readonly FileLoggerOptions _options;
private readonly ConcurrentQueue<LogEntry> _queue = new();
private readonly SemaphoreSlim _signal = new(0);
private readonly CancellationTokenSource _cts = new();
private readonly Task _writerTask;
private readonly ConcurrentDictionary<string, Lock> _fileLocks = new();
public FileLogWriter(FileLoggerOptions options)
{
_options = options;
Directory.CreateDirectory(_options.LogDirectory);
_writerTask = Task.Run(ProcessQueueAsync);
}
public void Enqueue(LogEntry entry)
{
if (entry.Level < _options.MinimumLevel)
return;
_queue.Enqueue(entry);
_signal.Release();
}
private async Task ProcessQueueAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
try
{
await _signal.WaitAsync(_cts.Token);
}
catch (OperationCanceledException)
{
break;
}
DrainQueue();
}
DrainQueue();
}
private void DrainQueue()
{
while (_queue.TryDequeue(out var entry))
{
try
{
WriteEntry(entry);
}
catch
{
// Logging darf die Anwendung niemals crashen
}
}
}
private void WriteEntry(LogEntry entry)
{
var dateDir = Path.Combine(
_options.LogDirectory,
entry.Timestamp.ToString(_options.DateFormat));
Directory.CreateDirectory(dateDir);
var safeModule = SanitizeModuleName(entry.Module);
var filePath = Path.Combine(dateDir, $"{safeModule}.log");
var fileLock = _fileLocks.GetOrAdd(filePath, _ => new Lock());
lock (fileLock)
{
var line = entry.Format(_options.TimestampFormat) + Environment.NewLine;
File.AppendAllText(filePath, line, Encoding.UTF8);
}
}
private static string SanitizeModuleName(string module)
{
var sanitized = module
.Replace('.', '_')
.Replace('/', '_')
.Replace('\\', '_');
foreach (var c in Path.GetInvalidFileNameChars())
sanitized = sanitized.Replace(c, '_');
return string.IsNullOrWhiteSpace(sanitized) ? "Unknown" : sanitized;
}
public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
try
{
await _writerTask;
}
catch (OperationCanceledException)
{
}
_signal.Dispose();
_cts.Dispose();
}
}
@@ -0,0 +1,18 @@
namespace ClawdDotNet.Core.Logging;
public sealed class FileLoggerOptions
{
public string LogDirectory { get; set; } = "./Logs";
public LogLevel MinimumLevel { get; set; } = LogLevel.Info;
public int MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10 MB
public string DateFormat { get; set; } = "yyyy-MM-dd";
public string TimestampFormat { get; set; } = "HH:mm:ss.fff";
}
public enum LogLevel
{
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
}
@@ -0,0 +1,66 @@
using System.Collections.Concurrent;
using MEL = Microsoft.Extensions.Logging;
namespace ClawdDotNet.Core.Logging;
/// <summary>
/// ILoggerProvider-Implementierung für das dateibasierte Logging.
/// Erzeugt pro Kategorie (= Modul) einen eigenen ModuleLogger.
///
/// Kategorien werden auf Modulnamen gemappt:
/// "ClawdDotNet.Core.Engine.AgentEngine" → "Core"
/// "ClawdDotNet.Tools.Database.DatabaseTool" → "Tool_Database"
/// "ClawdDotNet.Tools.FileRW.FileRWTool" → "Tool_FileRW"
/// Alles andere → letzter Namespace-Teil oder "General"
/// </summary>
public sealed class FileLoggerProvider : MEL.ILoggerProvider
{
private readonly FileLogWriter _writer;
private readonly FileLoggerOptions _options;
private readonly ConcurrentDictionary<string, ModuleLogger> _loggers = new();
public FileLoggerProvider(FileLoggerOptions options)
{
_options = options;
_writer = new FileLogWriter(options);
}
public MEL.ILogger CreateLogger(string categoryName)
{
var module = MapCategoryToModule(categoryName);
return _loggers.GetOrAdd(module, m => new ModuleLogger(m, _writer, _options));
}
internal static string MapCategoryToModule(string categoryName)
{
if (string.IsNullOrWhiteSpace(categoryName))
return "General";
// ClawdDotNet.Core.* → "Core"
if (categoryName.StartsWith("ClawdDotNet.Core.", StringComparison.Ordinal))
return "Core";
// ClawdDotNet.Host.* → "Host"
if (categoryName.StartsWith("ClawdDotNet.Host.", StringComparison.Ordinal)
|| categoryName == "ClawdDotNet.Host")
return "Host";
// ClawdDotNet.Tools.{ToolName}.* → "Tool_{ToolName}"
if (categoryName.StartsWith("ClawdDotNet.Tools.", StringComparison.Ordinal))
{
var afterTools = categoryName["ClawdDotNet.Tools.".Length..];
var dotIndex = afterTools.IndexOf('.');
var toolName = dotIndex > 0 ? afterTools[..dotIndex] : afterTools;
return $"Tool_{toolName}";
}
// Fallback: letzter Segment-Teil
var lastDot = categoryName.LastIndexOf('.');
return lastDot >= 0 ? categoryName[(lastDot + 1)..] : categoryName;
}
public void Dispose()
{
_writer.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}
+29
View File
@@ -0,0 +1,29 @@
namespace ClawdDotNet.Core.Logging;
public sealed record LogEntry(
DateTime Timestamp,
LogLevel Level,
string Module,
string Message,
Exception? Exception = null
)
{
public string Format(string timestampFormat)
{
var levelTag = Level switch
{
LogLevel.Debug => "DBG",
LogLevel.Info => "INF",
LogLevel.Warn => "WRN",
LogLevel.Error => "ERR",
_ => "???"
};
var line = $"[{Timestamp.ToString(timestampFormat)}] [{levelTag}] {Message}";
if (Exception is not null)
line += Environment.NewLine + Exception.ToString();
return line;
}
}
@@ -0,0 +1,42 @@
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Core.Logging;
public static class LoggingExtensions
{
public static ILoggerFactory AddClawdFileLogging(
this ILoggerFactory factory,
FileLoggerOptions? options = null)
{
factory.AddProvider(new FileLoggerProvider(options ?? new FileLoggerOptions()));
return factory;
}
public static ILoggerFactory CreateClawdLoggerFactory(
string logDirectory = "./Logs",
LogLevel minimumLevel = LogLevel.Info)
{
var options = new FileLoggerOptions
{
LogDirectory = logDirectory,
MinimumLevel = minimumLevel
};
var factory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(MapToMelLevel(minimumLevel));
});
factory.AddClawdFileLogging(options);
return factory;
}
private static Microsoft.Extensions.Logging.LogLevel MapToMelLevel(LogLevel level) => level switch
{
LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
LogLevel.Info => Microsoft.Extensions.Logging.LogLevel.Information,
LogLevel.Warn => Microsoft.Extensions.Logging.LogLevel.Warning,
LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
_ => Microsoft.Extensions.Logging.LogLevel.Information
};
}
@@ -0,0 +1,62 @@
using MEL = Microsoft.Extensions.Logging;
namespace ClawdDotNet.Core.Logging;
/// <summary>
/// Implementiert Microsoft.Extensions.Logging.ILogger und leitet alle
/// Einträge an den zentralen FileLogWriter weiter.
/// Jede Instanz ist einem Modul zugeordnet (z.B. "Core", "Tool_Database").
/// </summary>
public sealed class ModuleLogger : MEL.ILogger
{
private readonly string _module;
private readonly FileLogWriter _writer;
private readonly FileLoggerOptions _options;
public ModuleLogger(string module, FileLogWriter writer, FileLoggerOptions options)
{
_module = module;
_writer = writer;
_options = options;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(MEL.LogLevel logLevel)
{
var mapped = MapLevel(logLevel);
return mapped >= _options.MinimumLevel;
}
public void Log<TState>(
MEL.LogLevel logLevel,
MEL.EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
return;
var message = formatter(state, exception);
var entry = new LogEntry(
DateTime.Now,
MapLevel(logLevel),
_module,
message,
exception);
_writer.Enqueue(entry);
}
private static LogLevel MapLevel(MEL.LogLevel level) => level switch
{
MEL.LogLevel.Trace => LogLevel.Debug,
MEL.LogLevel.Debug => LogLevel.Debug,
MEL.LogLevel.Information => LogLevel.Info,
MEL.LogLevel.Warning => LogLevel.Warn,
MEL.LogLevel.Error => LogLevel.Error,
MEL.LogLevel.Critical => LogLevel.Error,
_ => LogLevel.Info
};
}