using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient(); var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); // Workspace-Root für Dateioperationen var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); var workspaceRoot = Path.Combine(dataDir, "workspace"); Directory.CreateDirectory(workspaceRoot); var toolDefs = new[] { new { name = "read_file", description = "Liest den Inhalt einer Datei. Gibt den Textinhalt zurück.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Dateipfad" } }, required = new[] { "path" } } }, new { name = "write_file", description = "Erstellt oder überschreibt eine Datei mit dem angegebenen Inhalt.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Dateipfad" }, ["content"] = new { type = "string", description = "Dateiinhalt" } }, required = new[] { "path", "content" } } }, new { name = "edit_file", description = "Ersetzt einen exakten Textabschnitt in einer Datei. oldText muss exakt matchen.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Dateipfad" }, ["old_text"] = new { type = "string", description = "Exakter Text der ersetzt werden soll" }, ["new_text"] = new { type = "string", description = "Neuer Text" } }, required = new[] { "path", "old_text", "new_text" } } }, new { name = "list_files", description = "Listet Dateien und Ordner in einem Verzeichnis auf.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Verzeichnispfad (leer = Root)" } }, required = System.Array.Empty() } } }; string ResolvePath(string relativePath) { var full = Path.GetFullPath(Path.Combine(workspaceRoot, relativePath ?? "")); if (!full.StartsWith(workspaceRoot)) throw new System.Exception("Zugriff außerhalb des Workspace nicht erlaubt"); return full; } string ExecuteTool(string name, JsonElement input) { switch (name) { case "read_file": { var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); if (!File.Exists(path)) return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspaceRoot, path)}"; return File.ReadAllText(path); } case "write_file": { var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); var dir = Path.GetDirectoryName(path); if (dir != null) Directory.CreateDirectory(dir); var content = input.GetProperty("content").GetString() ?? ""; File.WriteAllText(path, content); return $"Datei geschrieben: {Path.GetRelativePath(workspaceRoot, path)} ({content.Length} Zeichen)"; } case "edit_file": { var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); if (!File.Exists(path)) return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspaceRoot, path)}"; var fileContent = File.ReadAllText(path); var oldText = input.GetProperty("old_text").GetString() ?? ""; var newText = input.GetProperty("new_text").GetString() ?? ""; if (!fileContent.Contains(oldText)) return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig."; fileContent = fileContent.Replace(oldText, newText); File.WriteAllText(path, fileContent); return $"Datei bearbeitet: {Path.GetRelativePath(workspaceRoot, path)}"; } case "list_files": { var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""); if (!Directory.Exists(path)) return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(workspaceRoot, path)}"; var entries = new List(); foreach (var dir in Directory.GetDirectories(path)) entries.Add(Path.GetRelativePath(workspaceRoot, dir) + "/"); foreach (var file in Directory.GetFiles(path)) entries.Add(Path.GetRelativePath(workspaceRoot, file)); return entries.Count == 0 ? "(leer)" : string.Join("\n", entries); } default: return $"Fehler: Unbekanntes Tool: {name}"; } } app.MapPost("/api/chat", async (HttpRequest req, IHttpClientFactory httpFactory, IConfiguration config) => { var body = await JsonSerializer.DeserializeAsync(req.Body); if (body == null || body.Messages == null || body.Messages.Count == 0) return Results.BadRequest(new { error = "messages required" }); var token = config["AnthropicToken"]; if (string.IsNullOrEmpty(token)) return Results.Problem("AnthropicToken not configured"); var modelId = body.Model switch { "opus" => "claude-opus-4-6", "sonnet" => "claude-sonnet-4-6", _ => "claude-sonnet-4-6" }; // Messages in API-Format konvertieren var messages = new List(); foreach (var msg in body.Messages) { messages.Add(new { role = msg.Role, content = msg.Content }); } var systemPrompt = new List { new { type = "text", text = "You are Claude Code, Anthropic's official CLI for Claude." } }; if (!string.IsNullOrWhiteSpace(body.SystemPrompt)) { systemPrompt.Add(new { type = "text", text = body.SystemPrompt }); } var steps = new List(); long totalInputTokens = 0; long totalOutputTokens = 0; int iterations = 0; int maxIterations = 25; while (iterations < maxIterations) { iterations++; var payload = new Dictionary { ["model"] = modelId, ["max_tokens"] = 4096, ["system"] = systemPrompt, ["messages"] = messages, ["tools"] = toolDefs }; var client = httpFactory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); client.DefaultRequestHeaders.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"); client.DefaultRequestHeaders.Add("User-Agent", "claude-cli/2.1.2 (external, cli)"); client.DefaultRequestHeaders.Add("x-app", "cli"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Add("anthropic-dangerous-direct-browser-access", "true"); var json = JsonSerializer.Serialize(payload); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); var resp = await client.PostAsync("https://api.anthropic.com/v1/messages", httpContent); var respBody = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) return Results.Problem($"Anthropic API error: {(int)resp.StatusCode} - {respBody}"); var result = JsonSerializer.Deserialize(respBody); // Usage tracken if (result.TryGetProperty("usage", out var usage)) { if (usage.TryGetProperty("input_tokens", out var it)) totalInputTokens += it.GetInt64(); if (usage.TryGetProperty("output_tokens", out var ot)) totalOutputTokens += ot.GetInt64(); } var stopReason = result.GetProperty("stop_reason").GetString(); var contentBlocks = result.GetProperty("content"); // Assistant-Antwort zur History hinzufügen var assistantContent = new List(); var textParts = new List(); foreach (var block in contentBlocks.EnumerateArray()) { var blockType = block.GetProperty("type").GetString(); if (blockType == "text") { var text = block.GetProperty("text").GetString() ?? ""; textParts.Add(text); assistantContent.Add(new { type = "text", text }); } else if (blockType == "tool_use") { var toolId = block.GetProperty("id").GetString() ?? ""; var toolName = block.GetProperty("name").GetString() ?? ""; var toolInput = block.GetProperty("input"); assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = toolInput }); } } messages.Add(new { role = "assistant", content = assistantContent }); // Zwischen-Text als Step erfassen if (textParts.Count > 0) { steps.Add(new StepInfo { Type = "text", Text = string.Join("\n", textParts) }); } // Kein Tool Use? Fertig! if (stopReason != "tool_use") { return Results.Ok(new { text = string.Join("\n", textParts), model = modelId, inputTokens = totalInputTokens, outputTokens = totalOutputTokens, iterations, steps }); } // Tool Calls ausführen var toolResults = new List(); foreach (var block in contentBlocks.EnumerateArray()) { if (block.GetProperty("type").GetString() != "tool_use") continue; var toolId = block.GetProperty("id").GetString() ?? ""; var toolName = block.GetProperty("name").GetString() ?? ""; var toolInput = block.GetProperty("input"); string toolResult; bool isError = false; try { toolResult = ExecuteTool(toolName, toolInput); } catch (System.Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } steps.Add(new StepInfo { Type = "tool", ToolName = toolName, ToolInput = toolInput.ToString(), ToolResult = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult, IsError = isError }); toolResults.Add(new { type = "tool_result", tool_use_id = toolId, content = toolResult, is_error = isError }); } messages.Add(new { role = "user", content = toolResults }); } return Results.Problem("Max iterations erreicht"); }); app.Run("http://0.0.0.0:19000"); class ChatMessage { [JsonPropertyName("role")] public string Role { get; set; } = ""; [JsonPropertyName("content")] public string Content { get; set; } = ""; } class ChatRequest { [JsonPropertyName("model")] public string Model { get; set; } = "sonnet"; [JsonPropertyName("messages")] public List Messages { get; set; } = new(); [JsonPropertyName("systemPrompt")] public string SystemPrompt { get; set; } = ""; } class StepInfo { [JsonPropertyName("type")] public string Type { get; set; } = ""; [JsonPropertyName("text")] public string Text { get; set; } = ""; [JsonPropertyName("toolName")] public string ToolName { get; set; } = ""; [JsonPropertyName("toolInput")] public string ToolInput { get; set; } = ""; [JsonPropertyName("toolResult")] public string ToolResult { get; set; } = ""; [JsonPropertyName("isError")] public bool IsError { get; set; } }