using System.Text.Json; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Security; using ClawdDotNet.Core.Storage; using ClawdDotNet.Models; namespace ClawdDotNet.Services; /// /// Verwaltet die gesamte Verzeichnisstruktur für Instanzen und Agenten. /// /// Layout: /// {InstancesDir}/ /// ├── Instance-{Name}/ /// │ ├── InstanceSettings.json /// │ ├── TokenUsage.json /// │ └── Agents/ /// │ ├── AgentList.json /// │ └── Agent-{Name}/ /// │ ├── AgentSettings.json /// │ ├── Soul.md /// │ ├── Identity.md /// │ ├── Logs/ /// │ └── Workspace/ /// public sealed class InstanceDirectoryManager { private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true, PropertyNameCaseInsensitive = true }; private readonly string _instancesDir; private readonly Lock _tokenUsageLock = new(); public InstanceDirectoryManager(string instancesDirectory) { _instancesDir = Path.GetFullPath(instancesDirectory); Directory.CreateDirectory(_instancesDir); } public string InstancesDirectory => _instancesDir; // ═══════════════════════════════════════════════════ // INSTANZ-OPERATIONEN // ═══════════════════════════════════════════════════ public static string BuildInstanceFolderName(string instanceName) => $"Instance-{SanitizeName(instanceName)}"; public string GetInstancePath(string instanceName) => Path.Combine(_instancesDir, BuildInstanceFolderName(instanceName)); public List ListInstances() { var result = new List(); if (!Directory.Exists(_instancesDir)) return result; foreach (var dir in Directory.GetDirectories(_instancesDir, "Instance-*")) { var folderName = Path.GetFileName(dir); var settingsPath = Path.Combine(dir, "InstanceSettings.json"); var info = new InstanceInfo { FolderName = folderName, FolderPath = dir, InstanceName = folderName.Replace("Instance-", "") }; if (File.Exists(settingsPath)) { try { var json = File.ReadAllText(settingsPath); var config = JsonSerializer.Deserialize(json, JsonOpts); if (config is not null) { info.InstanceName = config.InstanceName; info.ApiKeyStatus = string.IsNullOrWhiteSpace(config.OpenRouterApiKey) ? "Fehlt" : "Konfiguriert"; } } catch { /* defekte Config → Standardwerte */ } } // Agenten zählen var agentsDir = Path.Combine(dir, "Agents"); if (Directory.Exists(agentsDir)) info.AgentCount = Directory.GetDirectories(agentsDir, "Agent-*").Length; result.Add(info); } return result.OrderBy(i => i.InstanceName).ToList(); } public string CreateInstance(string instanceName) { var instanceDir = GetInstancePath(instanceName); if (Directory.Exists(instanceDir)) throw new InvalidOperationException($"Instanz '{instanceName}' existiert bereits."); // Hauptverzeichnis Directory.CreateDirectory(instanceDir); // Agents-Unterverzeichnis var agentsDir = Path.Combine(instanceDir, "Agents"); Directory.CreateDirectory(agentsDir); // SharedWorkspace-Verzeichnis Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace")); // InstanceSettings.json var config = new InstanceConfig { InstanceId = Guid.NewGuid().ToString("N")[..8], InstanceName = instanceName, LogDirectory = "./Logs", WorkingDirectory = instanceDir }; SaveJson(Path.Combine(instanceDir, "InstanceSettings.json"), config); // TokenUsage.json (leer) var tokenUsage = new TokenUsageFile { InstanceId = config.InstanceId, InstanceName = instanceName }; SaveJson(Path.Combine(instanceDir, "TokenUsage.json"), tokenUsage); // AgentList.json (leer) SaveJson(Path.Combine(agentsDir, "AgentList.json"), new AgentListFile()); return instanceDir; } public InstanceConfig LoadInstanceConfig(string instanceDir) { var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json"); if (!File.Exists(settingsPath)) throw new FileNotFoundException($"InstanceSettings.json nicht gefunden in: {instanceDir}"); var json = File.ReadAllText(settingsPath); var config = JsonSerializer.Deserialize(json, JsonOpts) ?? throw new InvalidOperationException("InstanceSettings.json ist leer oder ungültig."); // Agenten aus Verzeichnisstruktur laden config.Agents.Clear(); var agentsDir = Path.Combine(instanceDir, "Agents"); // Sicherstellen, dass SharedWorkspace existiert (Migration) Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace")); // AgentList.json für Descriptions laden var agentListPath = Path.Combine(agentsDir, "AgentList.json"); var agentList = File.Exists(agentListPath) ? LoadJson(agentListPath) ?? new AgentListFile() : new AgentListFile(); if (Directory.Exists(agentsDir)) { foreach (var agentDir in Directory.GetDirectories(agentsDir, "Agent-*").OrderBy(d => d)) { var agent = LoadAgentConfig(agentDir); var folderName = Path.GetFileName(agentDir); var listEntry = agentList.Agents.FirstOrDefault(a => a.FolderName == folderName); if (listEntry is not null && !string.IsNullOrWhiteSpace(listEntry.Description)) agent.Description = listEntry.Description; config.Agents.Add(agent); } } return config; } public void SaveInstanceConfig(string instanceDir, InstanceConfig config) { var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json"); // Zugangsdaten nur für das Schreiben verschlüsseln — die laufende Instanz // braucht sie danach wieder im Klartext. ConfigSecrets.Protect(config); try { SaveJson(settingsPath, config); } finally { ConfigSecrets.Unprotect(config); } } // ═══════════════════════════════════════════════════ // AGENTEN-OPERATIONEN // ═══════════════════════════════════════════════════ public static string BuildAgentFolderName(string agentName) => $"Agent-{SanitizeName(agentName)}"; public string GetAgentPath(string instanceDir, string agentName) => Path.Combine(instanceDir, "Agents", BuildAgentFolderName(agentName)); public string CreateAgent(string instanceDir, string agentName, string description = "") { var agentDir = GetAgentPath(instanceDir, agentName); if (Directory.Exists(agentDir)) throw new InvalidOperationException($"Agent '{agentName}' existiert bereits in dieser Instanz."); // Verzeichnisse anlegen Directory.CreateDirectory(agentDir); Directory.CreateDirectory(Path.Combine(agentDir, "Logs")); Directory.CreateDirectory(Path.Combine(agentDir, "Workspace")); // AgentSettings.json var agentConfig = new AgentConfig { AgentId = SanitizeName(agentName).ToLowerInvariant(), DisplayName = agentName, Model = "anthropic/claude-sonnet-4-5" }; SaveAgentSettings(agentDir, agentConfig); // Identity.md AtomicFile.WriteAllText(Path.Combine(agentDir, "Identity.md"), $""" # Identity: {agentName} Du bist **{agentName}**, ein spezialisierter KI-Agent im ClawdDotNet-System. ## Rolle [Beschreibe hier die Rolle und Verantwortlichkeiten des Agenten] ## Expertise [Beschreibe hier die Fachgebiete und Fähigkeiten] ## Kontext [Beschreibe hier den Arbeitskontext und die Teamzugehörigkeit] """); // Soul.md AtomicFile.WriteAllText(Path.Combine(agentDir, "Soul.md"), $""" # Soul: {agentName} ## Persönlichkeit - Gründlich und zuverlässig - Klar und präzise in der Kommunikation - Proaktiv bei der Problemerkennung ## Arbeitsweise - Analysiere Aufgaben sorgfältig bevor du handelst - Dokumentiere deine Entscheidungen und Ergebnisse - Nutze die dir zugewiesenen Tools effizient ## Werte - Genauigkeit vor Geschwindigkeit - Transparenz in der Entscheidungsfindung - Sicherheit und Datenschutz haben Priorität """); // AgentList.json aktualisieren UpdateAgentList(instanceDir, agentName, description, BuildAgentFolderName(agentName)); return agentDir; } public AgentConfig LoadAgentConfig(string agentDir) { // AgentSettings.json laden var settingsPath = Path.Combine(agentDir, "AgentSettings.json"); AgentConfig config; if (File.Exists(settingsPath)) { var json = File.ReadAllText(settingsPath); config = JsonSerializer.Deserialize(json, JsonOpts) ?? new AgentConfig(); ConfigLoader.Migrate(config); ConfigSecrets.Unprotect(config); } else { config = new AgentConfig { AgentId = Path.GetFileName(agentDir).Replace("Agent-", "").ToLowerInvariant(), DisplayName = Path.GetFileName(agentDir).Replace("Agent-", "") }; } // Identity.md laden var identityPath = Path.Combine(agentDir, "Identity.md"); if (File.Exists(identityPath)) config.Identity = File.ReadAllText(identityPath); // Soul.md laden var soulPath = Path.Combine(agentDir, "Soul.md"); if (File.Exists(soulPath)) config.Soul = File.ReadAllText(soulPath); // Agent-Verzeichnis merken (stabil auch bei DisplayName-Änderungen) config.AgentDir = Path.GetFullPath(agentDir); // Workspace-Pfad setzen config.WorkspacePath = Path.GetFullPath(Path.Combine(agentDir, "Workspace")); // SharedWorkspace-Pfad setzen var agentsDir = Path.GetDirectoryName(agentDir); // Dies ist der /Agents Ordner if (agentsDir != null) { config.SharedWorkspacePath = Path.GetFullPath(Path.Combine(agentsDir, "SharedWorkspace")); } return config; } public void SaveAgentSettings(string agentDir, AgentConfig config) { var settingsPath = Path.Combine(agentDir, "AgentSettings.json"); ConfigSecrets.Protect(config); try { SaveJson(settingsPath, config); } finally { ConfigSecrets.Unprotect(config); } } public void SaveAgentIdentity(string agentDir, string identity) { AtomicFile.WriteAllText(Path.Combine(agentDir, "Identity.md"), identity); } public void SaveAgentSoul(string agentDir, string soul) { AtomicFile.WriteAllText(Path.Combine(agentDir, "Soul.md"), soul); } public void RemoveAgent(string instanceDir, string agentFolderName) { var agentDir = Path.Combine(instanceDir, "Agents", agentFolderName); if (Directory.Exists(agentDir)) Directory.Delete(agentDir, recursive: true); // AgentList.json aktualisieren var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json"); if (File.Exists(agentListPath)) { var list = LoadJson(agentListPath) ?? new AgentListFile(); list.Agents.RemoveAll(a => a.FolderName == agentFolderName); SaveJson(agentListPath, list); } } // ═══════════════════════════════════════════════════ // TOKEN USAGE // ═══════════════════════════════════════════════════ public void AppendTokenUsage(string instanceDir, TokenUsageRecord record) { lock (_tokenUsageLock) { var path = Path.Combine(instanceDir, "TokenUsage.json"); TokenUsageFile file; if (File.Exists(path)) { try { file = LoadJson(path) ?? new TokenUsageFile(); } catch (JsonException) { // Korrupte Datei: Backup erstellen, neu anfangen var backupPath = path + $".corrupt_{DateTime.Now:yyyyMMdd_HHmmss}"; try { File.Copy(path, backupPath, overwrite: true); } catch { /* best effort */ } file = new TokenUsageFile(); } } else { file = new TokenUsageFile(); } file.Records.Add(record); SaveJson(path, file); } } public TokenUsageFile LoadTokenUsage(string instanceDir) { var path = Path.Combine(instanceDir, "TokenUsage.json"); return File.Exists(path) ? LoadJson(path) ?? new TokenUsageFile() : new TokenUsageFile(); } // ═══════════════════════════════════════════════════ // HILFSMETHODEN // ═══════════════════════════════════════════════════ private void UpdateAgentList(string instanceDir, string agentName, string description, string folderName) { var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json"); var list = File.Exists(agentListPath) ? LoadJson(agentListPath) ?? new AgentListFile() : new AgentListFile(); // Duplikat-Check if (list.Agents.All(a => a.FolderName != folderName)) { list.Agents.Add(new AgentListItem { Name = agentName, Description = description, FolderName = folderName }); } SaveJson(agentListPath, list); } private static void SaveJson(string path, T obj) { // Atomar: Ein Absturz mitten im Schreiben soll keine halbe Datei hinterlassen. // Genau das ist bereits passiert (TokenUsage.json.corrupt_…). AtomicFile.WriteAllText(path, JsonSerializer.Serialize(obj, JsonOpts)); } private static T? LoadJson(string path) { // Lesen ohne den Schreiber zu blockieren — siehe AtomicFile.ReadAllText. var json = AtomicFile.ReadAllText(path); return JsonSerializer.Deserialize(json, JsonOpts); } private static string SanitizeName(string name) { var sanitized = name.Trim(); foreach (var c in Path.GetInvalidFileNameChars()) sanitized = sanitized.Replace(c, '_'); return sanitized.Replace(' ', '_'); } }