Initial commit: ClawdDotNet

Import des bestehenden Projektstands in Git.
- .NET 10 WinForms Anwendung (Multi-Agent / Tool-System)
- .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-26 18:21:46 +02:00
co-authored by Claude Opus 4.8
commit 2fed388c99
154 changed files with 29736 additions and 0 deletions
+695
View File
@@ -0,0 +1,695 @@
# 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: "1K15K" | "15K50K" | 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.**