# ClawdDotNet – Prompt-Anhang: Internet-Tools

Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert drei
unabhängige Internet-Tools sowie ein spezialisiertes Monitoring-Tool für
strukturierte Webseiten (z.B. Capitol Trades).

Alle Tools folgen den bekannten Prinzipien: IAgentTool implementiert,
Konfiguration ausschließlich aus AgentToolContext, keine Tool-zu-Tool-Abhängigkeiten.

---

## Pflicht-Regel: Timestamp in jedem ToolResult

**Diese Regel gilt für ALLE Internet-Tools ohne Ausnahme.**

Jedes `ToolResult.Content` das Finanzdaten, Nachrichten oder externe Daten enthält,
muss ein JSON-Objekt zurückgeben das mindestens enthält:

```json
{
  "fetchedAt":  "2026-05-13T07:42:00Z",
  "dataAsOf":   "2026-05-13T07:40:00Z",
  "source":     "https://...",
  "data":       { ... }
}
```

- `fetchedAt` = Zeitpunkt des HTTP-Requests (UTC, immer `DateTime.UtcNow`)
- `dataAsOf`  = Zeitpunkt der Daten laut Quelle (aus Response-Header, HTML oder API-Feld)
                Falls nicht ermittelbar: `null` — NIEMALS schätzen oder weglassen
- `source`    = exakte URL die abgerufen wurde

Der System-Prompt jedes Agenten der Internet-Tools nutzt MUSS enthalten:
> "Verwende niemals Daten ohne `fetchedAt`-Feld. Wenn `dataAsOf` null ist,
>  teile dem Nutzer mit dass der Datenzeitpunkt unbekannt ist.
>  Erfinde niemals Kurse, Preise oder Daten aus dem Gedächtnis."

---

## Tool 1: DirectAPI — Echtzeit-Finanzdaten

**Datei: `ClawdDotNet.Tools.DirectAPI/DirectApiTool.cs`**

Direkter HTTP-Zugriff auf Finanz-APIs. Kein HTML-Parsing.
Strukturiertes JSON mit verifizierbaren Timestamps.

### AgentConfig-Beispiel

```json
"DirectAPI": {
  "providers": {
    "twelvedata": {
      "apiKey": "your-key-here",
      "baseUrl": "https://api.twelvedata.com"
    },
    "alphavantage": {
      "apiKey": "your-key-here",
      "baseUrl": "https://www.alphavantage.co"
    },
    "coingecko": {
      "baseUrl": "https://api.coingecko.com/api/v3"
    },
    "yahoo": {
      "baseUrl": "https://query1.finance.yahoo.com"
    }
  },
  "defaultProvider": "twelvedata",
  "cacheTtlSeconds": 60
}
```

### Tool-Implementierung

```csharp
namespace ClawdDotNet.Tools.DirectAPI;

public sealed class DirectApiTool : IAgentTool
{
    public string Name        => "DirectAPI";
    public string Description => """
        Ruft Echtzeit-Finanzdaten von verifizierten APIs ab.
        Alle Antworten enthalten fetchedAt und dataAsOf Timestamps.
        Aktionen: quote, history, crypto, forex, search
        """;

    public JsonElement InputSchema => JsonDocument.Parse("""
    {
      "type": "object",
      "required": ["action", "symbol"],
      "properties": {
        "action": {
          "type": "string",
          "enum": ["quote", "history", "crypto", "forex", "search"],
          "description": "quote=aktueller Kurs, history=Kursverlauf, crypto=Krypto, forex=Wechselkurs, search=Symbol suchen"
        },
        "symbol":   { "type": "string", "description": "z.B. NVDA, BTC, EUR/USD" },
        "provider": { "type": "string", "description": "optional: twelvedata|alphavantage|coingecko|yahoo" },
        "interval": { "type": "string", "description": "für history: 1min|5min|1h|1day" },
        "outputsize":{ "type": "integer","description": "für history: Anzahl Datenpunkte, max 500" }
      }
    }
    """).RootElement;

    public async Task<ToolResult> ExecuteAsync(
        JsonElement input, AgentToolContext ctx, CancellationToken ct)
    {
        // Config aus Context lesen — nie aus statischen Feldern
        var config   = ctx.ToolConfig["DirectAPI"] as Dictionary<string, object?>
                       ?? throw new InvalidOperationException("DirectAPI config missing");

        var providers = config["providers"] as Dictionary<string, object?> ?? new();
        var cacheTtl  = Convert.ToInt32(config.GetValueOrDefault("cacheTtlSeconds") ?? 60);

        var action   = input.GetProperty("action").GetString()!;
        var symbol   = input.GetProperty("symbol").GetString()!;
        var provider = input.TryGetProperty("provider", out var p)
                       ? p.GetString()
                       : config.GetValueOrDefault("defaultProvider")?.ToString()
                         ?? "twelvedata";

        // Cache-Check: agentId + symbol + action als Key
        var cacheKey = $"directapi:{ctx.AgentId}:{provider}:{action}:{symbol}";
        // (Cache-Implementierung über IMemoryCache oder Redis aus Core)

        return action switch
        {
            "quote"   => await FetchQuoteAsync(symbol, provider, providers, ct),
            "history" => await FetchHistoryAsync(input, symbol, provider, providers, ct),
            "crypto"  => await FetchCryptoAsync(symbol, providers, ct),
            "forex"   => await FetchForexAsync(symbol, provider, providers, ct),
            "search"  => await SearchSymbolAsync(symbol, provider, providers, ct),
            _         => new ToolResult(false, "", $"Unknown action: {action}")
        };
    }

    private async Task<ToolResult> FetchQuoteAsync(
        string symbol, string provider,
        Dictionary<string, object?> providers, CancellationToken ct)
    {
        // Jeder Provider hat eigene URL-Struktur
        // Gemeinsam: immer Cache-Control: no-cache Header setzen
        // Gemeinsam: dataAsOf aus Response extrahieren (nicht schätzen)

        // Twelve Data Quote:
        // GET https://api.twelvedata.com/quote?symbol={symbol}&apikey={key}
        // Response enthält: "datetime" → das ist dataAsOf
        // Response enthält: "timestamp" (Unix) → ebenfalls verwertbar

        // Yahoo Finance Quote (kein Key nötig):
        // GET https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1m&range=1d
        // Response: result[0].meta.regularMarketTime (Unix timestamp) → dataAsOf

        // Alpha Vantage Quote:
        // GET https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={key}
        // Response: "Global Quote"."07. latest trading day" → dataAsOf (nur Datum, keine Zeit)

        // IMPLEMENTIERUNGSREGEL: dataAsOf IMMER aus der API-Antwort lesen.
        // Wenn das Feld fehlt oder leer ist → dataAsOf = null, NICHT DateTime.UtcNow.

        throw new NotImplementedException("Implement per provider");
    }

    // history, crypto, forex, search analog implementieren
}
```

### NuGet

Keine externen HTTP-Bibliotheken. Nur `System.Net.Http.HttpClient` via `IHttpClientFactory`.

---

## Tool 2: WebFetch — Nachrichten & strukturiertes HTML

**Datei: `ClawdDotNet.Tools.WebFetch/WebFetchTool.cs`**

HTTP-Abruf mit Whitelist, Timestamp-Extraktion und HTML-zu-Text-Konvertierung.
Kein JavaScript-Rendering (statisches HTML only).

### AgentConfig-Beispiel

```json
"WebFetch": {
  "allowedDomains": [
    "reuters.com",
    "bloomberg.com",
    "sec.gov",
    "feeds.finance.yahoo.com",
    "reddit.com",
    "capitoltrades.com"
  ],
  "maxResponseKb": 512,
  "timeoutSeconds": 15,
  "userAgent": "ClawdDotNet-Agent/1.0 (Research Bot)"
}
```

### Tool-Implementierung

```csharp
namespace ClawdDotNet.Tools.WebFetch;

public sealed class WebFetchTool : IAgentTool
{
    public string Name        => "WebFetch";
    public string Description => """
        Ruft statische Webseiten oder RSS-Feeds ab und extrahiert Text + Timestamps.
        Nur Domains aus der Whitelist erlaubt. Kein JavaScript-Rendering.
        Aktionen: fetch, rss
        """;

    public JsonElement InputSchema => JsonDocument.Parse("""
    {
      "type": "object",
      "required": ["action", "url"],
      "properties": {
        "action": {
          "type": "string",
          "enum": ["fetch", "rss"],
          "description": "fetch=HTML-Seite abrufen und zu Text konvertieren, rss=RSS/Atom-Feed parsen"
        },
        "url":     { "type": "string" },
        "selector":{ "type": "string",
                     "description": "optional: CSS-ähnlicher Hint welcher Teil relevant ist, z.B. 'table', 'article'" }
      }
    }
    """).RootElement;

    public async Task<ToolResult> ExecuteAsync(
        JsonElement input, AgentToolContext ctx, CancellationToken ct)
    {
        var config = ctx.ToolConfig["WebFetch"] as Dictionary<string, object?> ?? new();
        var allowedDomains = (config.GetValueOrDefault("allowedDomains")
                              as List<string>) ?? new List<string>();

        var url    = input.GetProperty("url").GetString()!;
        var action = input.GetProperty("action").GetString()!;

        // Domain-Whitelist prüfen
        var host = new Uri(url).Host.Replace("www.", "");
        if (!allowedDomains.Any(d => host == d || host.EndsWith("." + d)))
            return new ToolResult(false, "",
                $"Domain '{host}' nicht in der Whitelist dieses Agenten.");

        return action switch
        {
            "fetch" => await FetchPageAsync(url, input, config, ct),
            "rss"   => await FetchRssAsync(url, ct),
            _       => new ToolResult(false, "", $"Unknown action: {action}")
        };
    }

    private async Task<ToolResult> FetchPageAsync(
        string url, JsonElement input,
        Dictionary<string, object?> config, CancellationToken ct)
    {
        using var http    = CreateHttpClient(config);
        using var request = new HttpRequestMessage(HttpMethod.Get, url);

        // Kein Cache — immer frische Daten anfordern
        request.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
            { NoCache = true, NoStore = true };

        using var response = await http.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        // dataAsOf aus HTTP-Headern extrahieren (Reihenfolge: Last-Modified > Date)
        DateTimeOffset? dataAsOf = response.Content.Headers.LastModified
                                   ?? response.Headers.Date;

        var html    = await response.Content.ReadAsStringAsync(ct);
        var maxKb   = Convert.ToInt32(config.GetValueOrDefault("maxResponseKb") ?? 512);

        if (html.Length > maxKb * 1024)
            html = html[..(maxKb * 1024)];

        // HTML → lesbarer Text (einfache Implementierung ohne externe Libs)
        var text = StripHtml(html);

        // Versuche dataAsOf aus HTML-Meta-Tags zu verfeinern falls Header fehlt
        if (dataAsOf == null)
            dataAsOf = ExtractDateFromHtml(html);

        var result = new
        {
            fetchedAt = DateTime.UtcNow,
            dataAsOf  = dataAsOf?.UtcDateTime,
            source    = url,
            data      = new { text }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private async Task<ToolResult> FetchRssAsync(string url, CancellationToken ct)
    {
        // XML parsen mit System.Xml.Linq
        // Einträge: title, link, pubDate (→ dataAsOf), description
        // Neueste Einträge zuerst, max 20
        throw new NotImplementedException();
    }

    private static string StripHtml(string html)
    {
        // Einfaches Regex-basiertes Stripping
        // Script- und Style-Tags zuerst entfernen, dann alle anderen Tags
        // Anschließend HTML-Entities dekodieren (System.Net.WebUtility.HtmlDecode)
        // Mehrfache Leerzeilen auf max. 2 reduzieren
        throw new NotImplementedException();
    }

    private static DateTime? ExtractDateFromHtml(string html)
    {
        // Suche nach: <time datetime="...">, og:article:published_time,
        //             datePublished JSON-LD, <meta name="date" content="...">
        throw new NotImplementedException();
    }

    private static HttpClient CreateHttpClient(Dictionary<string, object?> config)
    {
        var client = new HttpClient();
        var timeout = Convert.ToInt32(config.GetValueOrDefault("timeoutSeconds") ?? 15);
        client.Timeout = TimeSpan.FromSeconds(timeout);
        client.DefaultRequestHeaders.UserAgent.ParseAdd(
            config.GetValueOrDefault("userAgent")?.ToString()
            ?? "ClawdDotNet-Agent/1.0");
        return client;
    }
}
```

### NuGet

`System.Xml.Linq` (im SDK enthalten) für RSS-Parsing. Kein externes HTML-Parser-Paket nötig.

---

## Tool 3: WebMonitor — Strukturiertes Seiten-Monitoring

**Datei: `ClawdDotNet.Tools.WebMonitor/WebMonitorTool.cs`**

Spezialisiert auf wiederkehrende Überprüfung von Seiten auf **neue Einträge**.
Speichert den zuletzt gesehenen Stand in der Datenbank und liefert nur Deltas.

Primärer Anwendungsfall: Capitol Trades, SEC-Filings, jede tabellarische Seite
mit eindeutigen IDs oder fortlaufenden Einträgen.

### Wie Capitol Trades funktioniert

Die Seite `https://www.capitoltrades.com/trades?pageSize=96` liefert:
- Sauber strukturiertes HTML mit einer Tabelle
- Jeder Trade hat eine eindeutige Trade-ID in der Detail-URL: `/trades/20003797558`
- Trade-IDs sind fortlaufend und numerisch aufsteigend
- Kein JavaScript-Rendering nötig — Daten sind im initialen HTML

Strategie: Höchste bekannte Trade-ID als Anker speichern. Bei jedem Check:
alle IDs auf Seite 1 extrahieren, mit gespeicherter Max-ID vergleichen,
nur neue Einträge melden.

### AgentConfig-Beispiel

```json
"WebMonitor": {
  "monitors": {
    "capitol_trades": {
      "url": "https://www.capitoltrades.com/trades?pageSize=96",
      "checkIntervalMinutes": 30,
      "idPattern": "/trades/(\\d+)",
      "idField": "tradeId",
      "parser": "capitol_trades",
      "alertOnNew": true,
      "storeHistory": true
    },
    "sec_filings": {
      "url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent&type=4&dateb=&owner=include&count=40",
      "checkIntervalMinutes": 60,
      "idPattern": "CIK=(\\d+)",
      "parser": "sec_form4",
      "alertOnNew": true,
      "storeHistory": false
    }
  }
}
```

### Tool-Implementierung

```csharp
namespace ClawdDotNet.Tools.WebMonitor;

public sealed class WebMonitorTool : IAgentTool
{
    public string Name        => "WebMonitor";
    public string Description => """
        Überwacht Webseiten auf neue Einträge und liefert nur die Deltas seit dem letzten Check.
        Speichert den Stand in der Datenbank. Ideal für Capitol Trades, SEC-Filings, etc.
        Aktionen: check, history, status
        """;

    public JsonElement InputSchema => JsonDocument.Parse("""
    {
      "type": "object",
      "required": ["action", "monitorId"],
      "properties": {
        "action": {
          "type": "string",
          "enum": ["check", "history", "status"],
          "description": "check=jetzt prüfen und Deltas liefern, history=bisherige Einträge, status=letzter Check-Zeitpunkt"
        },
        "monitorId": {
          "type": "string",
          "description": "z.B. capitol_trades, sec_filings — muss in Config definiert sein"
        },
        "limit": {
          "type": "integer",
          "description": "max. Anzahl Einträge für history, default 50"
        }
      }
    }
    """).RootElement;

    public async Task<ToolResult> ExecuteAsync(
        JsonElement input, AgentToolContext ctx, CancellationToken ct)
    {
        var config    = ctx.ToolConfig["WebMonitor"] as Dictionary<string, object?> ?? new();
        var monitors  = config["monitors"] as Dictionary<string, object?> ?? new();

        var action    = input.GetProperty("action").GetString()!;
        var monitorId = input.GetProperty("monitorId").GetString()!;

        if (!monitors.ContainsKey(monitorId))
            return new ToolResult(false, "",
                $"Monitor '{monitorId}' nicht in der Config dieses Agenten definiert.");

        var monitorConfig = monitors[monitorId] as Dictionary<string, object?> ?? new();

        return action switch
        {
            "check"   => await CheckForNewEntriesAsync(monitorId, monitorConfig, ctx, ct),
            "history" => await GetHistoryAsync(monitorId, input, ctx, ct),
            "status"  => await GetStatusAsync(monitorId, ctx, ct),
            _         => new ToolResult(false, "", $"Unknown action: {action}")
        };
    }

    private async Task<ToolResult> CheckForNewEntriesAsync(
        string monitorId, Dictionary<string, object?> monitorConfig,
        AgentToolContext ctx, CancellationToken ct)
    {
        var url       = monitorConfig["url"]?.ToString()!;
        var parser    = monitorConfig["parser"]?.ToString() ?? "generic";
        var idPattern = monitorConfig["idPattern"]?.ToString();

        // 1. Seite abrufen
        using var http    = new HttpClient();
        http.DefaultRequestHeaders.CacheControl =
            new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
        var html      = await http.GetStringAsync(url, ct);
        var fetchedAt = DateTime.UtcNow;

        // 2. Einträge parsen (parser-spezifisch)
        var entries = parser switch
        {
            "capitol_trades" => ParseCapitolTrades(html),
            "sec_form4"      => ParseSecForm4(html),
            _                => ParseGeneric(html, idPattern)
        };

        // 3. Letzte bekannte Max-ID aus State laden
        //    State-Key: "webmonitor:{agentId}:{monitorId}:maxId"
        //    State-Speicher: über den StateManager aus dem Core
        //    (wird als Dependency über AgentToolContext injiziert — siehe Core-Erweiterung unten)
        var stateKey   = $"webmonitor:{ctx.AgentId}:{monitorId}:maxId";
        var lastMaxId  = await ctx.StateStore.GetAsync(stateKey, ct);  // string? → long?
        var lastKnown  = long.TryParse(lastMaxId, out var l) ? l : 0L;

        // 4. Neue Einträge = alle mit ID > lastKnown
        var newEntries = entries
            .Where(e => e.NumericId > lastKnown)
            .OrderBy(e => e.NumericId)
            .ToList();

        // 5. Neue Max-ID persistieren
        if (newEntries.Count > 0)
        {
            var newMax = newEntries.Max(e => e.NumericId).ToString();
            await ctx.StateStore.SetAsync(stateKey, newMax, ct);

            // Optional: Einträge in History-Tabelle speichern
            if (monitorConfig.GetValueOrDefault("storeHistory") is true)
                await StoreHistoryAsync(monitorId, newEntries, ctx, ct);
        }

        // 6. Letzten Check-Zeitpunkt aktualisieren
        await ctx.StateStore.SetAsync(
            $"webmonitor:{ctx.AgentId}:{monitorId}:lastCheck",
            fetchedAt.ToString("O"), ct);

        // 7. Ergebnis
        var result = new
        {
            fetchedAt   = fetchedAt,
            dataAsOf    = fetchedAt,   // Capitol Trades: Seite ist immer aktuell
            source      = url,
            monitorId   = monitorId,
            newCount    = newEntries.Count,
            data        = new
            {
                newEntries = newEntries,
                message    = newEntries.Count == 0
                    ? "Keine neuen Einträge seit dem letzten Check."
                    : $"{newEntries.Count} neue Einträge gefunden."
            }
        };

        return new ToolResult(true, JsonSerializer.Serialize(result));
    }

    private static List<MonitorEntry> ParseCapitolTrades(string html)
    {
        // HTML-Tabelle parsen mit System.Text.RegularExpressions + String-Operationen
        //
        // Zu extrahieren pro Zeile:
        //   tradeId:    aus /trades/(\d+) in der Detail-URL → NumericId
        //   politician: Linktext des Politiker-Links
        //   party:      "Republican" | "Democrat" aus dem Text
        //   chamber:    "House" | "Senate"
        //   state:      2-Buchstaben-Code
        //   issuer:     Unternehmensname
        //   ticker:     z.B. "AVGO:US" → nur "AVGO"
        //   published:  Datum "8 May 2026" → DateTime
        //   traded:     Datum "27 Apr 2026" → DateTime
        //   filedAfterDays: Zahl aus "days N"
        //   owner:      "Undisclosed" | "Spouse" | "Joint" | etc.
        //   tradeType:  "buy" | "sell"
        //   size:       "1K–15K" | "15K–50K" | etc.
        //   price:      "$418.20" → decimal
        //   detailUrl:  vollständige URL

        // WICHTIG: Duplikate durch pageSize=96 möglich (selbe Transaktion, 2 IDs).
        // Beide IDs einliefern — der Agent entscheidet ob relevant.

        throw new NotImplementedException();
    }

    private static List<MonitorEntry> ParseSecForm4(string html)
        => throw new NotImplementedException();

    private static List<MonitorEntry> ParseGeneric(string html, string? idPattern)
        => throw new NotImplementedException();

    private Task StoreHistoryAsync(string monitorId,
        List<MonitorEntry> entries, AgentToolContext ctx, CancellationToken ct)
        => throw new NotImplementedException();

    private Task<ToolResult> GetHistoryAsync(string monitorId,
        JsonElement input, AgentToolContext ctx, CancellationToken ct)
        => throw new NotImplementedException();

    private Task<ToolResult> GetStatusAsync(string monitorId,
        AgentToolContext ctx, CancellationToken ct)
        => throw new NotImplementedException();
}

public sealed record MonitorEntry(
    long     NumericId,
    string   RawId,
    string   DetailUrl,
    DateTime? PublishedAt,
    DateTime? TradedAt,
    Dictionary<string, string> Fields   // flexible Felder je nach Parser
);
```

### Core-Erweiterung: IStateStore

`WebMonitor` braucht persistenten State zwischen Runs (die letzte bekannte Trade-ID).
Dafür muss `AgentToolContext` um ein `IStateStore` erweitert werden:

```csharp
// Core/Tools/AgentToolContext.cs — erweitern:
public sealed record AgentToolContext(
    string AgentId,
    string InstanceId,
    IReadOnlyDictionary<string, object?> ToolConfig,
    IStateStore StateStore,         // NEU
    ILogger Logger,
    CancellationToken CancellationToken
);

// Core/State/IStateStore.cs — neues Interface:
namespace ClawdDotNet.Core.State;

public interface IStateStore
{
    Task<string?> GetAsync(string key, CancellationToken ct);
    Task SetAsync(string key, string value, CancellationToken ct);
    Task DeleteAsync(string key, CancellationToken ct);
}

// Implementierungen (im Core oder als separates Projekt):
// - JsonFileStateStore  → speichert in ./data/{instanceId}/state.json
// - SqliteStateStore    → speichert in ./data/{instanceId}/state.db (empfohlen)
// Beide implementieren IStateStore.
// SqliteStateStore ist bevorzugt: atomic writes, kein Datenverlust bei Absturz.
// NuGet: Microsoft.Data.Sqlite
```

---

## Automatisches Monitoring via Scheduler

Das `WebMonitor`-Tool wird typischerweise nicht interaktiv genutzt, sondern
vom Scheduler getriggert. Konfiguration in der AgentConfig:

```json
{
  "agentId": "capitol-watcher",
  "displayName": "Capitol Trades Monitor",
  "model": "google/gemini-flash-1.5",
  "systemPrompt": "Du überwachst Politiker-Trades auf Capitol Trades. Bei neuen Trades analysierst du: Welcher Sektor? Auffälliges Timing? Cluster mehrerer Politiker beim selben Wert? Fasse neue Trades prägnant zusammen. Verwende niemals Daten ohne fetchedAt-Feld.",
  "tools": {
    "WebMonitor": {
      "monitors": {
        "capitol_trades": {
          "url": "https://www.capitoltrades.com/trades?pageSize=96",
          "checkIntervalMinutes": 30,
          "idPattern": "/trades/(\\d+)",
          "parser": "capitol_trades",
          "alertOnNew": true,
          "storeHistory": true
        }
      }
    },
    "Database": {
      "connectionString": "...",
      "allowedTables": ["capitol_trades_history", "trade_alerts"]
    },
    "Mail": {
      "smtpHost": "...",
      "allowedRecipients": ["owner@example.com"]
    }
  },
  "scheduler": {
    "cron": "*/30 * * * *",
    "runOnStart": true
  },
  "loopGuard": {
    "maxSteps": 10,
    "maxTokens": 30000
  }
}
```

Ablauf eines automatischen Runs:
1. Scheduler feuert alle 30 Minuten
2. Agent ruft `WebMonitor.check(capitol_trades)` auf
3. Tool liefert neue Trades als Delta
4. Agent analysiert: Cluster? Insider-Timing? Sektor-Häufung?
5. Bei relevanten Funden: `Mail.send` oder `Database.insert` zur Archivierung
6. Run beendet — kein manueller Eingriff nötig

---

## Implementierungsreihenfolge (für Claude Code)

Bearbeite diesen Abschnitt nach Abschluss der Core- und WinForms-Phase:

1. `IStateStore` Interface + `SqliteStateStore` Implementierung in Core anlegen
2. `AgentToolContext` um `IStateStore` erweitern, alle bestehenden Tool-Calls anpassen
3. `DirectApiTool` implementieren:
   - Twelve Data quote + history
   - Yahoo Finance quote (kein Key nötig, als Fallback)
   - CoinGecko crypto
   - Timestamp-Extraktion aus jeweiligem Response-Format
4. `WebFetchTool` implementieren:
   - Domain-Whitelist-Check
   - HttpClient mit no-cache Headers
   - Einfaches HTML-Stripping (ohne externe Libs)
   - RSS/Atom-Parser mit System.Xml.Linq
   - Timestamp-Extraktion aus HTML-Meta-Tags
5. `WebMonitorTool` implementieren:
   - `ParseCapitolTrades` als ersten Parser (Regex auf HTML-Tabelle)
   - `CheckForNewEntriesAsync` mit IStateStore-Integration
   - `GetHistory` und `GetStatus` Aktionen
   - `ParseSecForm4` als zweiten Parser
6. xUnit-Tests:
   - `ParseCapitolTrades` gegen gespeichertes HTML-Sample testen
   - Delta-Logik: lastKnownId=X, neue IDs=[X-1, X, X+1, X+2] → nur X+1 und X+2
   - Domain-Whitelist: erlaubte und gesperrte Domain testen
   - Timestamp-Extraktion: Last-Modified Header, og:article:published_time, kein Header
7. Beispiel-Config `capitol-team.json` anlegen
8. In `Program.cs`: WebMonitorTool registrieren, SqliteStateStore als IStateStore in DI

**Beginne mit Schritt 1 dieses Abschnitts.**
