using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Gott.Endpoints; public static class SessionEndpoint { public static void MapSessionEndpoints(this IEndpointRouteBuilder app) { app.MapGet("/api/sessions", HandleList); app.MapGet("/api/sessions/{sessionId}", HandleGet); app.MapPost("/api/sessions", HandleCreate); app.MapPut("/api/sessions/{sessionId}", HandleUpdate); app.MapDelete("/api/sessions/{sessionId}", HandleDelete); app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt); app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming); app.MapPost("/api/sessions/{sessionId}/exec", HandleExec); app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); } private static async Task HandleList(HttpContext ctx, DatabaseService db) { var sessions = await db.GetSessionsWithStats(); await ctx.Response.WriteAsJsonAsync(sessions); } private static async Task HandleGet(HttpContext ctx, DatabaseService db) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var session = await db.GetSession(sessionId); if (session == null) { ctx.Response.StatusCode = 404; return; } await ctx.Response.WriteAsJsonAsync(session); } private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService) { var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); var name = body.TryGetProperty("name", out var n) ? n.GetString() : "Neue Session"; var description = body.TryGetProperty("description", out var d) ? d.GetString() : null; var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null; var image = imageService.ResizeDataUrl(imageRaw); var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null; var workspace = body.TryGetProperty("workspace", out var w) ? w.GetString() : null; var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null; var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null; var session = await db.CreateSession(name, description, image, agentId, workspace, context, customCommands); await ctx.Response.WriteAsJsonAsync(session); } private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); await db.UpdateSession(sessionId, body, imageService); var session = await db.GetSession(sessionId); await ctx.Response.WriteAsJsonAsync(session); } private static async Task HandleDelete(HttpContext ctx, DatabaseService db) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } await db.DeleteSession(sessionId); ctx.Response.StatusCode = 204; } private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var session = await db.GetSession(sessionId); if (session == null) { ctx.Response.StatusCode = 404; return; } if (string.IsNullOrEmpty(session.AgentId)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" }); return; } var agent = await db.GetAgent(session.AgentId); if (agent == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" }); return; } var markedMessages = await db.GetMarkedMessages(sessionId); var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId); var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir); ctx.Response.ContentType = "text/plain; charset=utf-8"; await ctx.Response.WriteAsync(systemPrompt); } private static async Task HandleStreaming(HttpContext ctx, StreamingTracker streamingTracker) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var isStreaming = streamingTracker.IsStreaming(sessionId); var chainId = streamingTracker.GetChainId(sessionId); await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId }); } private static async Task HandleExec(HttpContext ctx, DatabaseService db) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var session = await db.GetSession(sessionId); if (session == null) { ctx.Response.StatusCode = 404; return; } var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); var command = body.TryGetProperty("command", out var c) ? c.GetString() : null; var mode = body.TryGetProperty("mode", out var m) ? m.GetString() ?? "background" : "background"; if (string.IsNullOrWhiteSpace(command)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Befehl angegeben" }); return; } var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : "/tmp"; if (mode == "foreground") { // Synchron ausführen, auf Ergebnis warten try { var psi = new ProcessStartInfo { FileName = "/bin/bash", WorkingDirectory = workingDir, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var proc = System.Diagnostics.Process.Start(psi); if (proc == null) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = "Prozess konnte nicht gestartet werden" }); return; } await proc.StandardInput.WriteAsync(command); proc.StandardInput.Close(); using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30)); try { await proc.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { proc.Kill(entireProcessTree: true); await ctx.Response.WriteAsJsonAsync(new { status = "timeout", output = "Timeout nach 30 Sekunden", exitCode = -1 }); return; } var stdout = await proc.StandardOutput.ReadToEndAsync(); var stderr = await proc.StandardError.ReadToEndAsync(); var output = new System.Text.StringBuilder(); if (!string.IsNullOrEmpty(stdout)) output.Append(stdout); if (!string.IsNullOrEmpty(stderr)) { if (output.Length > 0) output.Append("\n--- stderr ---\n"); output.Append(stderr); } await ctx.Response.WriteAsJsonAsync(new { status = "completed", output = output.ToString(), exitCode = proc.ExitCode }); } catch (Exception ex) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }); } } else { // Fire-and-forget via systemd-run try { var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh"; await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n"); var psi = new ProcessStartInfo { FileName = "systemd-run", Arguments = $"--scope --quiet bash {scriptPath}", WorkingDirectory = workingDir, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; var process = Process.Start(psi); if (process != null) { process.WaitForExit(500); } ctx.Response.StatusCode = 202; await ctx.Response.WriteAsJsonAsync(new { status = "started", command = command }); } catch (Exception ex) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }); } } } private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db) { var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var session = await db.GetSession(sessionId); if (session == null) { ctx.Response.StatusCode = 404; return; } var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : null; if (string.IsNullOrEmpty(workingDir)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); return; } try { var psi = new ProcessStartInfo { FileName = "git", Arguments = "status --porcelain", WorkingDirectory = workingDir, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var proc = Process.Start(psi); if (proc == null) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = "Git-Prozess konnte nicht gestartet werden" }); return; } var stdout = await proc.StandardOutput.ReadToEndAsync(); var stderr = await proc.StandardError.ReadToEndAsync(); await proc.WaitForExitAsync(); if (proc.ExitCode != 0) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = stderr.Trim() }); return; } var add = new List(); var edit = new List(); var delete = new List(); var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line.Length < 4) continue; var x = line[0]; // index status var y = line[1]; // working tree status var file = line.Substring(3); // Bei Renames: "R old -> new" => nur den neuen Namen nehmen var arrowIdx = file.IndexOf(" -> ", StringComparison.Ordinal); if (arrowIdx >= 0) file = file.Substring(arrowIdx + 4); if (x == '?' && y == '?') { add.Add(file); } else if (x == 'D' || y == 'D') { delete.Add(file); } else if (x == 'A') { add.Add(file); } else { edit.Add(file); } } // Outgoing commits zählen (nur wenn Remote existiert) int outgoing = 0; try { var outPsi = new ProcessStartInfo { FileName = "git", Arguments = "rev-list --count @{u}..HEAD", WorkingDirectory = workingDir, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var outProc = Process.Start(outPsi); if (outProc != null) { var outResult = (await outProc.StandardOutput.ReadToEndAsync()).Trim(); await outProc.WaitForExitAsync(); if (outProc.ExitCode == 0 && int.TryParse(outResult, out var count)) outgoing = count; } } catch { // Kein Remote oder kein Upstream – outgoing bleibt 0 } await ctx.Response.WriteAsJsonAsync(new { add, edit, delete, outgoing }); } catch (Exception ex) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }); } } }