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; 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(); var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); var workspaceRoot = Path.Combine(dataDir, "workspace"); Directory.CreateDirectory(workspaceRoot); // ── Tool Definitionen ── 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() } } }; // ── Hilfsfunktionen ── string ResolvePath(string relativePath) { var full = Path.GetFullPath(Path.Combine(workspaceRoot, relativePath ?? "")); if (!full.StartsWith(workspaceRoot)) throw new 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}"; } } async Task WriteSseEvent(HttpResponse response, string eventType, object data) { var json = JsonSerializer.Serialize(data); await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n"); await response.Body.FlushAsync(); } HttpClient CreateAnthropicClient(IHttpClientFactory factory, string token) { var client = factory.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"); return client; } // ── SSE Streaming Endpoint ── app.MapPost("/api/chat", async (HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config) => { var body = await JsonSerializer.DeserializeAsync(req.Body); if (body == null || body.Messages == null || body.Messages.Count == 0) { res.StatusCode = 400; await res.WriteAsJsonAsync(new { error = "messages required" }); return; } var token = config["AnthropicToken"]; if (string.IsNullOrEmpty(token)) { res.StatusCode = 500; await res.WriteAsJsonAsync(new { error = "AnthropicToken not configured" }); return; } var modelId = body.Model switch { "opus" => "claude-opus-4-6", "sonnet" => "claude-sonnet-4-6", _ => "claude-sonnet-4-6" }; // SSE Response starten res.Headers["Content-Type"] = "text/event-stream"; res.Headers["Cache-Control"] = "no-cache"; res.Headers["Connection"] = "keep-alive"; 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 }); 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, ["stream"] = true }; var client = CreateAnthropicClient(httpFactory, token); var json = JsonSerializer.Serialize(payload); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); var httpResp = await client.SendAsync( new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent }, HttpCompletionOption.ResponseHeadersRead ); if (!httpResp.IsSuccessStatusCode) { var errBody = await httpResp.Content.ReadAsStringAsync(); await WriteSseEvent(res, "error", new { error = $"API {(int)httpResp.StatusCode}: {errBody}" }); await WriteSseEvent(res, "done", new { }); return; } // Anthropic SSE Stream parsen var stream = await httpResp.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); var stopReason = ""; var assistantContent = new List(); var currentToolId = ""; var currentToolName = ""; var currentToolInputJson = new StringBuilder(); var currentTextBlock = new StringBuilder(); var blockIndex = -1; var blockTypes = new Dictionary(); var executedTools = new List<(string Id, string Result, bool IsError)>(); string line; while ((line = await reader.ReadLineAsync()) != null) { if (!line.StartsWith("data: ")) continue; var data = line["data: ".Length..]; if (string.IsNullOrWhiteSpace(data)) continue; JsonElement evt; try { evt = JsonSerializer.Deserialize(data); } catch { continue; } var evtType = evt.TryGetProperty("type", out var t) ? t.GetString() ?? "" : ""; switch (evtType) { case "message_start": { if (evt.TryGetProperty("message", out var msg) && msg.TryGetProperty("usage", out var u)) { if (u.TryGetProperty("input_tokens", out var it)) totalInputTokens += it.GetInt64(); } break; } case "content_block_start": { var idx = evt.GetProperty("index").GetInt32(); var cb = evt.GetProperty("content_block"); var cbType = cb.GetProperty("type").GetString() ?? ""; blockTypes[idx] = cbType; if (cbType == "text") { blockIndex = idx; currentTextBlock.Clear(); await WriteSseEvent(res, "text_start", new { index = idx }); } else if (cbType == "tool_use") { // Vorherigen Text-Block abschließen if (currentTextBlock.Length > 0) { assistantContent.Add(new { type = "text", text = currentTextBlock.ToString() }); currentTextBlock.Clear(); } blockIndex = idx; currentToolId = cb.GetProperty("id").GetString() ?? ""; currentToolName = cb.GetProperty("name").GetString() ?? ""; currentToolInputJson.Clear(); await WriteSseEvent(res, "tool_start", new { name = currentToolName, id = currentToolId }); } break; } case "content_block_delta": { var idx = evt.GetProperty("index").GetInt32(); var delta = evt.GetProperty("delta"); var deltaType = delta.GetProperty("type").GetString() ?? ""; if (deltaType == "text_delta") { var text = delta.GetProperty("text").GetString() ?? ""; currentTextBlock.Append(text); await WriteSseEvent(res, "text_delta", new { text }); } else if (deltaType == "input_json_delta") { var partial = delta.GetProperty("partial_json").GetString() ?? ""; currentToolInputJson.Append(partial); } break; } case "content_block_stop": { var idx = evt.GetProperty("index").GetInt32(); if (blockTypes.TryGetValue(idx, out var bt)) { if (bt == "text") { var text = currentTextBlock.ToString(); if (text.Length > 0) { assistantContent.Add(new { type = "text", text }); await WriteSseEvent(res, "text_end", new { }); } currentTextBlock.Clear(); } else if (bt == "tool_use") { JsonElement inputEl; try { inputEl = JsonSerializer.Deserialize(currentToolInputJson.ToString()); } catch { inputEl = JsonSerializer.Deserialize("{}"); } assistantContent.Add(new { type = "tool_use", id = currentToolId, name = currentToolName, input = inputEl }); // Tool sofort ausführen und streamen string toolResult; bool isError = false; try { toolResult = ExecuteTool(currentToolName, inputEl); } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } executedTools.Add((currentToolId, toolResult, isError)); await WriteSseEvent(res, "tool_end", new { name = currentToolName, input = currentToolInputJson.ToString(), result = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult, isError }); } } break; } case "message_delta": { if (evt.TryGetProperty("delta", out var d) && d.TryGetProperty("stop_reason", out var sr)) stopReason = sr.GetString() ?? ""; if (evt.TryGetProperty("usage", out var u2) && u2.TryGetProperty("output_tokens", out var ot)) totalOutputTokens += ot.GetInt64(); break; } } } // Restlichen Text abschließen if (currentTextBlock.Length > 0) assistantContent.Add(new { type = "text", text = currentTextBlock.ToString() }); // Assistant-Antwort zur History messages.Add(new { role = "assistant", content = assistantContent }); // Kein Tool Use? Fertig! if (stopReason != "tool_use") { await WriteSseEvent(res, "done", new { model = modelId, inputTokens = totalInputTokens, outputTokens = totalOutputTokens, iterations }); return; } // Tool Results zusammenbauen (wurden beim Streaming bereits ausgeführt) var toolResults = new List(); foreach (var tr in executedTools) { toolResults.Add(new { type = "tool_result", tool_use_id = tr.Id, content = tr.Result, is_error = tr.IsError }); } messages.Add(new { role = "user", content = toolResults }); } await WriteSseEvent(res, "error", new { error = "Max iterations erreicht" }); await WriteSseEvent(res, "done", new { }); }); 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; } = ""; }