Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/anqinou-art_nagibridge.json
T

1 line
32 KiB
JSON

{"repo": "anqinou-art/nagibridge", "files": {"LlmClient.cs": {"dac5f78ef6edc9f9f6c38f0c95ca59fc39d8a3f2": {"t": 1785746186.8943837, "n": 6212, "h": "26f6d16e56aa6bcb", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace NagiBridge;\n\npublic class LlmClient\n{\n private readonly ModConfig _config;\n private readonly HttpClient _http = new();\n private readonly List<ChatTurn> _history = new();\n private readonly string _historyPath;\n\n private record ChatTurn(string Role, string Content);\n\n public LlmClient(ModConfig config, string? modPath = null)\n {\n _config = config;\n _historyPath = modPath != null\n ? Path.Combine(modPath, \"chat_history.json\")\n : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, \"Mods\", \"NagiBridge\", \"chat_history.json\");\n LoadHistory();\n }\n\n private void LoadHistory()\n {\n try\n {\n if (File.Exists(_historyPath))\n {\n var json = File.ReadAllText(_historyPath, Encoding.UTF8);\n var entries = JsonSerializer.Deserialize<List<HistoryEntry>>(json);\n if (entries != null)\n {\n _history.Clear();\n foreach (var e in entries)\n _history.Add(new ChatTurn(e.role, e.content));\n }\n }\n }\n catch { }\n }\n\n private void SaveHistory()\n {\n try\n {\n var entries = new List<HistoryEntry>();\n foreach (var h in _history)\n entries.Add(new HistoryEntry { role = h.Role, content = h.Content });\n var json = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(_historyPath, json, Encoding.UTF8);\n }\n catch { }\n }\n\n private class HistoryEntry\n {\n public string role { get; set; } = \"\";\n public string content { get; set; } = \"\";\n }\n\n public async Task<string> SendAsync(string userMessage)\n {\n if (string.IsNullOrEmpty(_config.ApiKey))\n return \"[No API key - paste in API Setup]\";\n\n _history.Add(new ChatTurn(\"user\", userMessage));\n if (_history.Count > _config.MaxHistoryMessages)\n _history.RemoveAt(0);\n\n try\n {\n var customUrl = string.IsNullOrEmpty(_config.ApiUrl) ? null : _config.ApiUrl;\n var response = _config.ApiProvider.ToLower() switch\n {\n \"claude\" or \"anthropic\" => await CallClaude(customUrl),\n \"deepseek\" => await CallOpenAICompatible(customUrl ?? \"https://api.deepseek.com/v1/chat/completions\"),\n \"openai\" => await CallOpenAICompatible(customUrl ?? \"https://api.openai.com/v1/chat/completions\"),\n _ => await CallOpenAICompatible(customUrl ?? \"https://api.openai.com/v1/chat/completions\")\n };\n\n _history.Add(new ChatTurn(\"assistant\", response));\n if (_history.Count > _config.MaxHistoryMessages)\n _history.RemoveAt(0);\n\n SaveHistory();\n return response;\n }\n catch (Exception ex)\n {\n return $\"[Error: {ex.Message}]\";\n }\n }\n\n private async Task<string> CallClaude(string? customUrl = null)\n {\n var messages = new List<object>();\n foreach (var turn in _history)\n messages.Add(new { role = turn.Role, content = turn.Content });\n\n var body = new\n {\n model = _config.Model,\n max_tokens = 300,\n system = _config.SystemPrompt,\n messages\n };\n\n var request = new HttpRequestMessage(HttpMethod.Post, customUrl ?? \"https://api.anthropic.com/v1/messages\");\n request.Headers.Add(\"x-api-key\", _config.ApiKey);\n request.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, \"application/json\");\n\n var resp = await _http.SendAsync(request);\n var json = await resp.Content.ReadAsStringAsync();\n\n if (!resp.IsSuccessStatusCode)\n return $\"[API {(int)resp.StatusCode}: {TryExtractError(json)}]\";\n\n using var doc = JsonDocument.Parse(json);\n var content = doc.RootElement.GetProperty(\"content\");\n if (content.GetArrayLength() > 0)\n return content[0].GetProperty(\"text\").GetString() ?? \"\";\n return \"[Empty response]\";\n }\n\n private async Task<string> CallOpenAICompatible(string endpoint)\n {\n var messages = new List<object>();\n messages.Add(new { role = \"system\", content = _config.SystemPrompt });\n foreach (var turn in _history)\n messages.Add(new { role = turn.Role, content = turn.Content });\n\n var body = new\n {\n model = _config.Model,\n max_tokens = 300,\n messages\n };\n\n var request = new HttpRequestMessage(HttpMethod.Post, endpoint);\n request.Headers.Add(\"Authorization\", $\"Bearer {_config.ApiKey}\");\n request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, \"application/json\");\n\n var resp = await _http.SendAsync(request);\n var json = await resp.Content.ReadAsStringAsync();\n\n if (!resp.IsSuccessStatusCode)\n return $\"[API {(int)resp.StatusCode}: {TryExtractError(json)}]\";\n\n using var doc = JsonDocument.Parse(json);\n var choices = doc.RootElement.GetProperty(\"choices\");\n if (choices.GetArrayLength() > 0)\n return choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n return \"[Empty response]\";\n }\n\n private static string TryExtractError(string json)\n {\n try\n {\n using var doc = JsonDocument.Parse(json);\n if (doc.RootElement.TryGetProperty(\"error\", out var err))\n {\n if (err.ValueKind == JsonValueKind.Object && err.TryGetProperty(\"message\", out var msg))\n return msg.GetString() ?? \"unknown\";\n return err.GetString() ?? \"unknown\";\n }\n }\n catch { }\n return json.Length > 100 ? json[..100] : json;\n }\n}\n"}, "ac4b3d734c4c5d767460a51c7c6d3c9ed1689994": {"t": 1785760437.2097704, "n": 6212, "h": "26f6d16e56aa6bcb", "c": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace NagiBridge;\n\npublic class LlmClient\n{\n private readonly ModConfig _config;\n private readonly HttpClient _http = new();\n private readonly List<ChatTurn> _history = new();\n private readonly string _historyPath;\n\n private record ChatTurn(string Role, string Content);\n\n public LlmClient(ModConfig config, string? modPath = null)\n {\n _config = config;\n _historyPath = modPath != null\n ? Path.Combine(modPath, \"chat_history.json\")\n : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, \"Mods\", \"NagiBridge\", \"chat_history.json\");\n LoadHistory();\n }\n\n private void LoadHistory()\n {\n try\n {\n if (File.Exists(_historyPath))\n {\n var json = File.ReadAllText(_historyPath, Encoding.UTF8);\n var entries = JsonSerializer.Deserialize<List<HistoryEntry>>(json);\n if (entries != null)\n {\n _history.Clear();\n foreach (var e in entries)\n _history.Add(new ChatTurn(e.role, e.content));\n }\n }\n }\n catch { }\n }\n\n private void SaveHistory()\n {\n try\n {\n var entries = new List<HistoryEntry>();\n foreach (var h in _history)\n entries.Add(new HistoryEntry { role = h.Role, content = h.Content });\n var json = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true });\n File.WriteAllText(_historyPath, json, Encoding.UTF8);\n }\n catch { }\n }\n\n private class HistoryEntry\n {\n public string role { get; set; } = \"\";\n public string content { get; set; } = \"\";\n }\n\n public async Task<string> SendAsync(string userMessage)\n {\n if (string.IsNullOrEmpty(_config.ApiKey))\n return \"[No API key - paste in API Setup]\";\n\n _history.Add(new ChatTurn(\"user\", userMessage));\n if (_history.Count > _config.MaxHistoryMessages)\n _history.RemoveAt(0);\n\n try\n {\n var customUrl = string.IsNullOrEmpty(_config.ApiUrl) ? null : _config.ApiUrl;\n var response = _config.ApiProvider.ToLower() switch\n {\n \"claude\" or \"anthropic\" => await CallClaude(customUrl),\n \"deepseek\" => await CallOpenAICompatible(customUrl ?? \"https://api.deepseek.com/v1/chat/completions\"),\n \"openai\" => await CallOpenAICompatible(customUrl ?? \"https://api.openai.com/v1/chat/completions\"),\n _ => await CallOpenAICompatible(customUrl ?? \"https://api.openai.com/v1/chat/completions\")\n };\n\n _history.Add(new ChatTurn(\"assistant\", response));\n if (_history.Count > _config.MaxHistoryMessages)\n _history.RemoveAt(0);\n\n SaveHistory();\n return response;\n }\n catch (Exception ex)\n {\n return $\"[Error: {ex.Message}]\";\n }\n }\n\n private async Task<string> CallClaude(string? customUrl = null)\n {\n var messages = new List<object>();\n foreach (var turn in _history)\n messages.Add(new { role = turn.Role, content = turn.Content });\n\n var body = new\n {\n model = _config.Model,\n max_tokens = 300,\n system = _config.SystemPrompt,\n messages\n };\n\n var request = new HttpRequestMessage(HttpMethod.Post, customUrl ?? \"https://api.anthropic.com/v1/messages\");\n request.Headers.Add(\"x-api-key\", _config.ApiKey);\n request.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, \"application/json\");\n\n var resp = await _http.SendAsync(request);\n var json = await resp.Content.ReadAsStringAsync();\n\n if (!resp.IsSuccessStatusCode)\n return $\"[API {(int)resp.StatusCode}: {TryExtractError(json)}]\";\n\n using var doc = JsonDocument.Parse(json);\n var content = doc.RootElement.GetProperty(\"content\");\n if (content.GetArrayLength() > 0)\n return content[0].GetProperty(\"text\").GetString() ?? \"\";\n return \"[Empty response]\";\n }\n\n private async Task<string> CallOpenAICompatible(string endpoint)\n {\n var messages = new List<object>();\n messages.Add(new { role = \"system\", content = _config.SystemPrompt });\n foreach (var turn in _history)\n messages.Add(new { role = turn.Role, content = turn.Content });\n\n var body = new\n {\n model = _config.Model,\n max_tokens = 300,\n messages\n };\n\n var request = new HttpRequestMessage(HttpMethod.Post, endpoint);\n request.Headers.Add(\"Authorization\", $\"Bearer {_config.ApiKey}\");\n request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, \"application/json\");\n\n var resp = await _http.SendAsync(request);\n var json = await resp.Content.ReadAsStringAsync();\n\n if (!resp.IsSuccessStatusCode)\n return $\"[API {(int)resp.StatusCode}: {TryExtractError(json)}]\";\n\n using var doc = JsonDocument.Parse(json);\n var choices = doc.RootElement.GetProperty(\"choices\");\n if (choices.GetArrayLength() > 0)\n return choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n return \"[Empty response]\";\n }\n\n private static string TryExtractError(string json)\n {\n try\n {\n using var doc = JsonDocument.Parse(json);\n if (doc.RootElement.TryGetProperty(\"error\", out var err))\n {\n if (err.ValueKind == JsonValueKind.Object && err.TryGetProperty(\"message\", out var msg))\n return msg.GetString() ?? \"unknown\";\n return err.GetString() ?? \"unknown\";\n }\n }\n catch { }\n return json.Length > 100 ? json[..100] : json;\n }\n}\n"}, "ea4a6afbe6c1d22fb6900a918fdaae1077116ff3": {"t": 1785760439.4876904, "n": 4566, "h": "9181d03e3b20b122", "c": "using System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading.Tasks;\n\nnamespace NagiBridge;\n\npublic class LlmClient\n{\n private readonly ModConfig _config;\n private readonly HttpClient _http = new();\n private readonly List<ChatTurn> _history = new();\n\n private record ChatTurn(string Role, string Content);\n\n public LlmClient(ModConfig config)\n {\n _config = config;\n }\n\n public async Task<string> SendAsync(string userMessage)\n {\n if (string.IsNullOrEmpty(_config.ApiKey))\n return \"[No API key configured - edit config.json]\";\n\n _history.Add(new ChatTurn(\"user\", userMessage));\n if (_history.Count > _config.MaxHistoryMessages)\n _history.RemoveAt(0);\n\n try\n {\n var response = _config.ApiProvider.ToLower() switch\n {\n \"claude\" or \"anthropic\" => await CallClaude(),\n \"deepseek\" => await CallOpenAICompatible(\"https://api.deepseek.com/v1/chat/completions\"),\n \"openai\" => await CallOpenAICompatible(\"https://api.openai.com/v1/chat/completions\"),\n _ => await CallOpenAICompatible(\"https://api.openai.com/v1/chat/completions\")\n };\n\n _history.Add(new ChatTurn(\"assistant\", response));\n if (_history.Count > _config.MaxHistoryMessages)\n _history.RemoveAt(0);\n\n return response;\n }\n catch (Exception ex)\n {\n return $\"[Error: {ex.Message}]\";\n }\n }\n\n private async Task<string> CallClaude()\n {\n var messages = new List<object>();\n foreach (var turn in _history)\n messages.Add(new { role = turn.Role, content = turn.Content });\n\n var body = new\n {\n model = _config.Model,\n max_tokens = 300,\n system = _config.SystemPrompt,\n messages\n };\n\n var request = new HttpRequestMessage(HttpMethod.Post, \"https://api.anthropic.com/v1/messages\");\n request.Headers.Add(\"x-api-key\", _config.ApiKey);\n request.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, \"application/json\");\n\n var resp = await _http.SendAsync(request);\n var json = await resp.Content.ReadAsStringAsync();\n\n if (!resp.IsSuccessStatusCode)\n return $\"[API {(int)resp.StatusCode}: {TryExtractError(json)}]\";\n\n using var doc = JsonDocument.Parse(json);\n var content = doc.RootElement.GetProperty(\"content\");\n if (content.GetArrayLength() > 0)\n return content[0].GetProperty(\"text\").GetString() ?? \"\";\n return \"[Empty response]\";\n }\n\n private async Task<string> CallOpenAICompatible(string endpoint)\n {\n var messages = new List<object>();\n messages.Add(new { role = \"system\", content = _config.SystemPrompt });\n foreach (var turn in _history)\n messages.Add(new { role = turn.Role, content = turn.Content });\n\n var body = new\n {\n model = _config.Model,\n max_tokens = 300,\n messages\n };\n\n var request = new HttpRequestMessage(HttpMethod.Post, endpoint);\n request.Headers.Add(\"Authorization\", $\"Bearer {_config.ApiKey}\");\n request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, \"application/json\");\n\n var resp = await _http.SendAsync(request);\n var json = await resp.Content.ReadAsStringAsync();\n\n if (!resp.IsSuccessStatusCode)\n return $\"[API {(int)resp.StatusCode}: {TryExtractError(json)}]\";\n\n using var doc = JsonDocument.Parse(json);\n var choices = doc.RootElement.GetProperty(\"choices\");\n if (choices.GetArrayLength() > 0)\n return choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n return \"[Empty response]\";\n }\n\n private static string TryExtractError(string json)\n {\n try\n {\n using var doc = JsonDocument.Parse(json);\n if (doc.RootElement.TryGetProperty(\"error\", out var err))\n {\n if (err.ValueKind == JsonValueKind.Object && err.TryGetProperty(\"message\", out var msg))\n return msg.GetString() ?? \"unknown\";\n return err.GetString() ?? \"unknown\";\n }\n }\n catch { }\n return json.Length > 100 ? json[..100] : json;\n }\n}\n"}}, "README.md": {"dac5f78ef6edc9f9f6c38f0c95ca59fc39d8a3f2": {"t": 1785746378.549374, "n": 7022, "h": "92c7efbb2c8733ea", "c": "# NagiBridge\n\nStardew Valley SMAPI mod — HTTP API for external control + in-game AI chat.\n\n## Install (players)\n\n> ### 📥 [**Download the latest release →**](https://github.com/anqinou-art/NagiBridge/releases/latest)\n>\n> Grab `NagiBridge.zip` from the link above — it already contains the compiled `NagiBridge.dll`. **You do not need to build anything.**\n\n1. Install [SMAPI](https://smapi.io/)\n2. Download `NagiBridge.zip` from [**Releases**](https://github.com/anqinou-art/NagiBridge/releases/latest) and unzip it\n3. Move the unzipped `NagiBridge` folder into your `Stardew Valley/Mods/` folder\n (so you end up with `Stardew Valley/Mods/NagiBridge/NagiBridge.dll` + `manifest.json`)\n4. Launch the game through SMAPI\n\nFirst launch auto-generates `config.json`. The mod is built for **Stardew Valley 1.6+ / SMAPI 4.0+** and the same `.dll` works on **Windows, macOS and Linux**.\n\n> **Cloning the source repo?** There is **no `.dll` in the source tree** — it ships only in [Releases](https://github.com/anqinou-art/NagiBridge/releases/latest). If you cloned/downloaded the code and SMAPI says it can't find `NagiBridge.dll`, download the release zip above, or build it yourself (see below).\n\n## Build from source (developers)\n\nRequires the [.NET SDK](https://dotnet.microsoft.com/download) (6.0+) and a local Stardew Valley install.\n\n```bash\ndotnet build -c Release\n```\n\nThe project uses [`Pathoschild.Stardew.ModBuildConfig`](https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md), which **auto-detects your game folder** (Steam / GOG / Xbox on any OS) and copies the built mod straight into `Stardew Valley/Mods/NagiBridge/`. No paths to edit. If auto-detection fails, set a `GamePath` property or `GAME_PATH` environment variable pointing at your install.\n\n## HTTP API\n\nGame starts an HTTP server on `localhost:7842` (host) / `7843` (farmhand).\n\nFull endpoint list: see [AGENTS.md](AGENTS.md)\n\n---\n\n## In-Game Chat\n\nPress **`** (backtick, keyboard top-left) to open the chat panel.\n\n### First Open — Mode Selection\n\n```\n┌─────────────────────────┐\n│ Nagi Chat │\n│ │\n│ > API Mode │\n│ Channel Mode │\n│ │\n│ Up/Down = Select │\n│ Enter = OK │\n└─────────────────────────┘\n```\n\n- **API Mode** — Connect to an LLM API (Claude, DeepSeek, OpenAI, or any compatible endpoint). The AI chats with you in-game.\n- **Channel Mode** — Connect to Claude Code via a local channel server. Claude Code controls the game and chats through the panel.\n\nAfter selecting a mode, you'll be prompted to enter a **display name** for the AI (default: \"Nagi\").\n\n### Switching Modes\n\nPress **Tab** (when input is empty) in the chat panel to return to mode selection. Switch between API and Channel at any time.\n\n---\n\n### API Mode\n\nFor chatting with an LLM directly in-game. No external tools required.\n\n#### Setup\n\n1. Select **API Mode** → enter display name → press Enter\n2. **API Key** — Paste your key with `Ctrl+V` (shown as `****abcd`)\n3. **URL** — Auto-filled based on provider. Custom endpoints supported.\n4. Press **Enter** to connect\n\n#### Supported Providers\n\n| Provider | URL | Model |\n|----------|-----|-------|\n| DeepSeek | `https://api.deepseek.com/v1/chat/completions` | `deepseek-chat` |\n| Claude | `https://api.anthropic.com/v1/messages` | `claude-sonnet-4-6-20250514` |\n| OpenAI | `https://api.openai.com/v1/chat/completions` | `gpt-4o` |\n| Custom | Any OpenAI-compatible endpoint | Any model name |\n\nThe provider is auto-detected from the URL. Custom URLs use OpenAI-compatible format.\n\n#### Config (config.json)\n\n```json\n{\n \"Mode\": \"\",\n \"ApiProvider\": \"deepseek\",\n \"ApiUrl\": \"\",\n \"ApiKey\": \"\",\n \"Model\": \"deepseek-chat\",\n \"SystemPrompt\": \"You are a friendly AI companion in Stardew Valley...\",\n \"MaxHistoryMessages\": 20\n}\n```\n\n- **API key is saved locally** after first entry. Next launch auto-fills (masked with stars).\n- **Chat history persists** across game restarts in `chat_history.json`.\n- **SystemPrompt** — Customize the AI's personality.\n- **MaxHistoryMessages** — How many turns to include in API calls (memory window).\n\n#### Tool Calling Agent (Optional)\n\nFor LLMs that support tool calling (function calling), a standalone Python agent lets the AI actually play the game:\n\n```bash\npython scripts/tool_agent.py --provider deepseek --key sk-xxx --port 7842\n```\n\nThe AI can use 17 game tools: `get_state`, `warp`, `move_to`, `use_tool`, `farm`, `mine`, `harvest`, etc.\n\nSee [scripts/tool_agent.py](scripts/tool_agent.py) for details.\n\n---\n\n### Channel Mode\n\nFor connecting to Claude Code (CC). CC controls the game via HTTP API and chats through the panel.\n\n#### Architecture\n\n```\nGame ChatHud → POST → channel_server.py (:9000) → inbox file → CC Monitor → CC responds\n ↓\nGame ChatHud ← /chat/push (:7842) ←────────────────────────────────────────────┘\n```\n\n#### Setup (Claude Code side)\n\nEach new CC session needs two things:\n\n**1. Start channel server:**\n```bash\nbash ~/source/NagiBridge/scripts/start_channel.sh\n```\nOr manually:\n```bash\ncd scripts && python channel_server.py &\n```\n\n**2. Start message monitor** (CC tool):\n```\nMonitor(description=\"stardew chat\", persistent=true,\n command=\"tail -f ~/nagi/overlay_inbox.jsonl | grep --line-buffered text\")\n```\n\n**3. Reply to player** via API:\n```bash\ncurl -X POST http://localhost:7842/chat/push \\\n -H \"Content-Type: application/json\" \\\n -d '{\"sender\":\"Nagi\",\"message\":\"Hello!\"}'\n```\n\n#### Config\n\n```json\n{\n \"Mode\": \"cc\",\n \"ChannelServerUrl\": \"http://localhost:9000/chat\"\n}\n```\n\nWhen `Mode` is `\"cc\"`, the chat panel opens directly in Channel mode (skips mode selection).\n\n---\n\n### Chat Panel Controls\n\n| Key | Action |\n|-----|--------|\n| **`** | Open / close chat panel |\n| **Enter** | Send message |\n| **Tab** | Switch mode (when input is empty) |\n| **Ctrl+V** | Paste from clipboard |\n| **Scroll** | Browse message history |\n\n### Preview\n\nWhen the chat panel is closed, the last 2 messages are shown as a preview above the toolbar (bottom-left corner).\n\n---\n\n## Automation Scripts\n\nPython scripts for common farm tasks. Require `requests` package.\n\n```bash\npip install requests\n```\n\n| Script | Usage |\n|--------|-------|\n| `farm_row.py` | `python farm_row.py 64 18 10 --seed \"Melon Seeds\" --rows 3` |\n| `water_crops.py` | `python water_crops.py --port 7842` |\n| `harvest.py` | `python harvest.py 60 17 70 20 --sell` |\n| `mine_run.py` | `python mine_run.py --start-level 1 --max-levels 5` |\n| `chop_trees.py` | `python chop_trees.py --count 10` |\n| `clear_area.py` | `python clear_area.py 60 15 70 25` |\n| `pet_animals.py` | `python pet_animals.py` |\n| `keg_manager.py` | `python keg_manager.py --fruit \"Ancient Fruit\"` |\n| `furnace_manager.py` | `python furnace_manager.py --ore \"Copper Ore\"` |\n| `shop_buy.py` | `python shop_buy.py --items \"493:10,491:6\"` |\n\nAll scripts default to `--port 7842`. Add `PYTHONIOENCODING=utf-8` on Windows.\n"}, "93cfa8377e4f6f2dc9712a1cc218ef03ab2b80ff": {"t": 1785760438.6226244, "n": 5528, "h": "ffbeb8db7a72d284", "c": "# NagiBridge\n\nStardew Valley SMAPI mod — HTTP API for external control + in-game AI chat.\n\n## Install\n\n1. Install [SMAPI](https://smapi.io/)\n2. Copy `NagiBridge.dll` + `manifest.json` to `Stardew Valley/Mods/NagiBridge/`\n3. Launch game via SMAPI\n\nFirst launch auto-generates `config.json`.\n\n## HTTP API\n\nGame starts an HTTP server on `localhost:7842` (host) / `7843` (farmhand).\n\nFull endpoint list: see [AGENTS.md](AGENTS.md)\n\n---\n\n## In-Game Chat\n\nPress **`** (backtick, keyboard top-left) to open the chat panel.\n\n### First Open — Mode Selection\n\n```\n┌─────────────────────────┐\n│ Nagi Chat │\n│ │\n│ > API Mode │\n│ Channel Mode │\n│ │\n│ Up/Down = Select │\n│ Enter = OK │\n└─────────────────────────┘\n```\n\n- **API Mode** — Connect to an LLM API (Claude, DeepSeek, OpenAI, or any compatible endpoint). The AI chats with you in-game.\n- **Channel Mode** — Connect to Claude Code via a local channel server. Claude Code controls the game and chats through the panel.\n\nAfter selecting a mode, you'll be prompted to enter a **display name** for the AI (default: \"Nagi\").\n\n### Switching Modes\n\nPress **Tab** (when input is empty) in the chat panel to return to mode selection. Switch between API and Channel at any time.\n\n---\n\n### API Mode\n\nFor chatting with an LLM directly in-game. No external tools required.\n\n#### Setup\n\n1. Select **API Mode** → enter display name → press Enter\n2. **API Key** — Paste your key with `Ctrl+V` (shown as `****abcd`)\n3. **URL** — Auto-filled based on provider. Custom endpoints supported.\n4. Press **Enter** to connect\n\n#### Supported Providers\n\n| Provider | URL | Model |\n|----------|-----|-------|\n| DeepSeek | `https://api.deepseek.com/v1/chat/completions` | `deepseek-chat` |\n| Claude | `https://api.anthropic.com/v1/messages` | `claude-sonnet-4-6-20250514` |\n| OpenAI | `https://api.openai.com/v1/chat/completions` | `gpt-4o` |\n| Custom | Any OpenAI-compatible endpoint | Any model name |\n\nThe provider is auto-detected from the URL. Custom URLs use OpenAI-compatible format.\n\n#### Config (config.json)\n\n```json\n{\n \"Mode\": \"\",\n \"ApiProvider\": \"deepseek\",\n \"ApiUrl\": \"\",\n \"ApiKey\": \"\",\n \"Model\": \"deepseek-chat\",\n \"SystemPrompt\": \"You are a friendly AI companion in Stardew Valley...\",\n \"MaxHistoryMessages\": 20\n}\n```\n\n- **API key is saved locally** after first entry. Next launch auto-fills (masked with stars).\n- **Chat history persists** across game restarts in `chat_history.json`.\n- **SystemPrompt** — Customize the AI's personality.\n- **MaxHistoryMessages** — How many turns to include in API calls (memory window).\n\n#### Tool Calling Agent (Optional)\n\nFor LLMs that support tool calling (function calling), a standalone Python agent lets the AI actually play the game:\n\n```bash\npython scripts/tool_agent.py --provider deepseek --key sk-xxx --port 7842\n```\n\nThe AI can use 17 game tools: `get_state`, `warp`, `move_to`, `use_tool`, `farm`, `mine`, `harvest`, etc.\n\nSee [scripts/tool_agent.py](scripts/tool_agent.py) for details.\n\n---\n\n### Channel Mode\n\nFor connecting to Claude Code (CC). CC controls the game via HTTP API and chats through the panel.\n\n#### Architecture\n\n```\nGame ChatHud → POST → channel_server.py (:9000) → inbox file → CC Monitor → CC responds\n ↓\nGame ChatHud ← /chat/push (:7842) ←────────────────────────────────────────────┘\n```\n\n#### Setup (Claude Code side)\n\nEach new CC session needs two things:\n\n**1. Start channel server:**\n```bash\nbash ~/source/NagiBridge/scripts/start_channel.sh\n```\nOr manually:\n```bash\ncd scripts && python channel_server.py &\n```\n\n**2. Start message monitor** (CC tool):\n```\nMonitor(description=\"stardew chat\", persistent=true,\n command=\"tail -f ~/nagi/overlay_inbox.jsonl | grep --line-buffered text\")\n```\n\n**3. Reply to player** via API:\n```bash\ncurl -X POST http://localhost:7842/chat/push \\\n -H \"Content-Type: application/json\" \\\n -d '{\"sender\":\"Nagi\",\"message\":\"Hello!\"}'\n```\n\n#### Config\n\n```json\n{\n \"Mode\": \"cc\",\n \"ChannelServerUrl\": \"http://localhost:9000/chat\"\n}\n```\n\nWhen `Mode` is `\"cc\"`, the chat panel opens directly in Channel mode (skips mode selection).\n\n---\n\n### Chat Panel Controls\n\n| Key | Action |\n|-----|--------|\n| **`** | Open / close chat panel |\n| **Enter** | Send message |\n| **Tab** | Switch mode (when input is empty) |\n| **Ctrl+V** | Paste from clipboard |\n| **Scroll** | Browse message history |\n\n### Preview\n\nWhen the chat panel is closed, the last 2 messages are shown as a preview above the toolbar (bottom-left corner).\n\n---\n\n## Automation Scripts\n\nPython scripts for common farm tasks. Require `requests` package.\n\n```bash\npip install requests\n```\n\n| Script | Usage |\n|--------|-------|\n| `farm_row.py` | `python farm_row.py 64 18 10 --seed \"Melon Seeds\" --rows 3` |\n| `water_crops.py` | `python water_crops.py --port 7842` |\n| `harvest.py` | `python harvest.py 60 17 70 20 --sell` |\n| `mine_run.py` | `python mine_run.py --start-level 1 --max-levels 5` |\n| `chop_trees.py` | `python chop_trees.py --count 10` |\n| `clear_area.py` | `python clear_area.py 60 15 70 25` |\n| `pet_animals.py` | `python pet_animals.py` |\n| `keg_manager.py` | `python keg_manager.py --fruit \"Ancient Fruit\"` |\n| `furnace_manager.py` | `python furnace_manager.py --ore \"Copper Ore\"` |\n| `shop_buy.py` | `python shop_buy.py --items \"493:10,491:6\"` |\n\nAll scripts default to `--port 7842`. Add `PYTHONIOENCODING=utf-8` on Windows.\n"}, "d2edd4d338fb340722041f3f2735856d2db7411a": {"t": 1785760439.8051834, "n": 226, "h": "f9ea281ceb684097", "c": "# nagibridge\n\nTo install dependencies:\n\n```bash\nbun install\n```\n\nTo run:\n\n```bash\nbun run index.ts\n```\n\nThis project was created using `bun init` in bun v1.3.13. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.\n"}}}}