diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 000000000..681311eb9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..4ff375cfe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,313 @@ +# RS-Agent Bot Guide + +You're here to play the mmo game through the progressive development of botting scripts, starting small then adapting to your desires and ideas. +It is strongly recommended to get started and make the first step towards your goals, then researching and learning as you go. + +## First Time Setup + +**Create a new bot using the setup script:** + +Ask the user for a bot name (max 12 chars, alphanumeric). If they skip, use the command without a username to auto-generate a random 9-character name. + +```bash +# With custom username +bun bots/create-bot.ts {username} + +# Auto-generate random username +bun bots/create-bot.ts + +# Use local server (sets SERVER=localhost in bot.env) +bun bots/create-bot.ts {username} --local + +# Use a custom server +bun bots/create-bot.ts {username} --server=myserver.example.com +``` + +This automatically creates: + +- `bots/{username}/bot.env` - Credentials with auto-generated password +- `bots/{username}/lab_log.md` - Session notes template +- `bots/{username}/script.ts` - Ready-to-run starter script + +### Quick Start + +1. Install dependencies: `bun install` (from project root) +2. Open project in your AI coding tool — the MCP server will be available automatically +3. Control your bot with suggestions. + +### Tools + +| Tool | Description | +| ------------------------------ | ---------------------------------------------- | +| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. | +| `list_bots()` | List connected bots | +| `disconnect_bot(name)` | Disconnect a bot | + +### Example + +```typescript +// Just execute - auto-connects on first use +execute_code({ + bot_name: "mybot", + code: ` + const state = sdk.getState(); + console.log('Position:', state.player.worldX, state.player.worldZ); + + // Chop trees for 1 minute + const endTime = Date.now() + 60_000; + while (Date.now() < endTime) { + const tree = sdk.findNearbyLoc(/^tree$/i); + if (tree) await bot.chopTree(tree); + } + + return sdk.getInventory(); + `, +}); +``` + +See `mcp/README.md` for detailed API reference. + +Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals. + +## Reporting SDK Bugs + +When the SDK has a bug or rough edge, first find a workaround, then file a report. One command, no auth, no permission needed: + +```bash +bun sdk/bug-report.ts "incorrect results from bot.foo(), had to use raw sdk.sendFoo() instead for xyz reason." +``` + +Important note: The game itself is extremely well tested and complete, it's not buggy. If you can't figure out how to do something, don't blame the game, blame your assumptions and keep investigating. File a bug report _after_ you figure out +what's going on, don't just assume the game is broken because you got confused, and give up. The SDK is a thin layer on top of the game, and the game is the source of truth. + +After filing the bug, keep working on your goal! + +## Session Workflow + +This is a **persistent character** - you don't restart fresh each time. The workflow is: + +### 1. Check World State First + +Before writing any script, check where the bot is and what it has: + +```bash +bun sdk/cli.ts {username} +``` + +This shows: position, inventory, skills, nearby NPCs/objects, and more. + +**Exception**: Skip this if you just created the character and know it's at spawn. + +**Tutorial Check**: If the character is in the tutorial area, call `await bot.skipTutorial()` before running any other scripts. The tutorial blocks normal gameplay. + +### 2. Write Your Script + +Edit `bots/{username}/script_name.ts` with your goal. Keep scripts focused on one task. you may write multiple scripts for different tasks and switch between them. + +### 3. Run the Script + +```bash +bun bots/{username}/script_name.ts +``` + +### 4. Observe and Iterate + +Watch the output. After the script finishes (or fails), check state again: + +```bash +bun sdk/cli.ts {username} +``` + +Record observations in `lab_log.md`, then improve the script. + +## Script Duration Guidelines + +**Start short, extend as you gain confidence:** + +| Duration | Use When | +| ------------- | ----------------------------------------------------- | +| **10s** | New script, single actions, untested logic, debugging | +| **30s-1 min** | Validated approach, building confidence | +| **5+ min** | Proven strategy, grinding runs. USE SPARINGLY | + +A failed 5-minute run wastes more time than five 30 second diagnostic runs. **Fail fast and start simple.** + +Look out for "I can't reach" messages - the solution is often to open closed gates or that the item isn't accessible. + +Read and grep in the learnings/ and wiki/ folder for tips, skill guides, item and npc locations, and shop information. + +## SDK API Reference + +For the complete method reference, see **[sdk/API.md](sdk/API.md)** (auto-generated from source). + +**Quick overview:** + +- `bot.*` - High-level actions that attempt to observe method-specific evidence (chopTree, walkTo, attackNpc, etc.). Check a result when the method returns one. +- `sdk.*` - State queries and low-level browser-client dispatches. A successful `send*` result does not prove the game server applied the effect. + +### bot.\* Quick Reference + +| Method | What it does | +| ------------------------------- | ---------------------------------------------------------------- | +| `walkTo(x, z, tolerance?)` | Pathfind to coords, opens doors along the way | +| `talkTo(target)` | Walk to NPC, start dialog | +| `interactNpc(target, option?)` | Walk to NPC, interact with any option (e.g. `'trade'`, `'fish'`) | +| `interactLoc(target, option?)` | Walk to loc, interact with any option (e.g. `'mine'`, `'smelt'`) | +| `attackNpc(target)` | Walk to NPC, start combat | +| `pickpocketNpc(target)` | Pickpocket NPC, detects XP gain vs stun | +| `castSpellOnNpc(target, spell)` | Cast combat spell on NPC | +| `chopTree(target?)` | Chop tree, wait for logs | +| `pickupItem(target)` | Pick up ground item | +| `openDoor(target?)` | Open a door or gate | +| `openBank()` | Open nearest bank | +| `depositItem(target, amount?)` | Deposit item to bank | +| `withdrawItem(slot, amount?)` | Withdraw item from bank | +| `closeInterface()` | Close any open modal (shop, bank, book, quest scroll) | +| `closeShop()` / `closeBank()` | Close the shop / bank interface | +| `openShop(target?)` | Open shop via shopkeeper NPC | +| `buyFromShop(target, amount?)` | Buy item from open shop | +| `sellToShop(target, amount?)` | Sell item to open shop | +| `equipItem(target)` | Equip from inventory | +| `unequipItem(target)` | Unequip to inventory | +| `eatFood(target)` | Eat food, returns HP gained | +| `useItemOnLoc(item, loc)` | Use inventory item on loc (e.g. fish on range) | +| `useItemOnNpc(item, npc)` | Use inventory item on NPC | +| `burnLogs(target?)` | Light logs with tinderbox | +| `fletchLogs(product?)` | Fletch logs with knife | +| `craftLeather(product?)` | Craft leather with needle | +| `smithAtAnvil(product)` | Smith bars at anvil | +| `dismissBlockingUI()` | Dismiss level-up dialogs (called automatically by all actions) | +| `navigateDialog(choices)` | Auto-click through dialog options | +| `skipTutorial()` | Skip the tutorial island | + +### sdk.\* Commonly Used Directly + +| Method | What it does | +| --------------------------------------------------- | --------------------------------------------------- | +| `getState()` | Full world state snapshot | +| `getSkill(name)` / `getSkillXp(name)` | Skill info | +| `getInventory()` / `findInventoryItem(pattern)` | Inventory queries | +| `findNearbyNpc(pattern)` / `findNearbyLoc(pattern)` | Find nearby entities | +| `findGroundItem(pattern)` | Find ground items | +| `getDialog()` | Current dialog state | +| `sendClickDialog(option)` | Click dialog option by its published `index` | +| `clickDialogByText(pattern)` | Click dialog option by label — prefer this | +| `sendClickComponent(id)` | Click interface button | +| `sendCloseModal()` / `sendCloseShop()` | Close the open modal / shop | +| `sendDropItem(slot)` | Drop inventory item | +| `sendUseItem(slot)` | Use inventory item (bury, etc.) | +| `sendUseItemOnItem(src, dst)` | Combine two items | +| `say(text)` | Send chat, auto-chunked past the length cap | +| `getChat(opts?)` / `getNewChat(opts?)` | Read chat history (500-deep) / only unseen messages | +| `waitForChat(opts?)` | Wait for a message (`{from, matching, timeout}`) | +| `waitForCondition(pred)` | Wait for state predicate | +| `waitForTicks(n)` | Wait n game ticks | +| `scanNearbyLocs(radius?)` | Async extended-range loc scan; must be awaited | + +See `sdk/API.md` for exact async signatures, parameter defaults, and return +types. + +### Code environments + +MCP `execute_code` exposes `bot` and `sdk` directly. Standalone +`bots/{username}/*.ts` files must import `runScript` and receive +`{ bot, sdk }` in its callback. Do not paste runner-context expressions into +`execute_code`. + +Only one control connection should own a bot. Use observe mode for read-only +monitoring, avoid overlapping action programs on the same bot, and inspect +`sdk.getStateAge()` before acting after a disconnect or long pause. + +--- + +### Modals Hide the Inventory + +A shop or bank modal replaces the inventory tab, so the server rejects +inventory packets sent underneath it as "component not visible" — with no game +message. Close the modal before doing anything else: + +```typescript +await bot.closeInterface(); // any modal +await bot.closeShop(); // shop specifically +await sdk.sendCloseModal(); // low-level +``` + +Don't click the interface's own "Close Window" component id. It's a +client-side close button with no server trigger; the SDK now routes those to a +close for you, but the named methods above are what to reach for. + +### Dialog Options Are Not In Product Order + +Skill dialogs (fletching, crafting) publish four buttons _per product_ — Make +X, Make 10, Make 5, then one labelled with the product name. Arrow shafts are +option 4 of 12, not option 1. Never assume ordering: + +```typescript +const dialog = sdk.getDialog(); +console.log(dialog?.options.map((o) => `${o.index}: ${o.text}`)); +await sdk.clickDialogByText(/arrow shaft/i); // or bot.fletchLogs('arrow shaft') +``` + +### Dismiss Level-Up Dialogs + +All BotActions methods automatically dismiss blocking UI (level-up dialogs, etc.) before executing. You don't need to call this manually in loops. + +```typescript +// Only needed if using low-level sdk methods directly: +await bot.dismissBlockingUI(); + +// Or manually check +if (sdk.getState()?.dialog.isOpen) { + await sdk.sendClickDialog(0); +} +``` + +### Error Handling + +```typescript +const result = await bot.chopTree(); +if (!result.success) { + console.log(`Failed: ${result.message}`); + // Handle failure - maybe walk somewhere else +} +``` + +## Project Structure + +``` + +bots/ +└── {username}/ + ├── bot.env # Credentials (BOT_USERNAME, PASSWORD, SERVER) + ├── lab_log.md # Session notes and observations + └── script.ts # Current script + +sdk/ +├── index.ts # BotSDK (low-level) +├── actions.ts # BotActions (high-level) +├── cli.ts # CLI for checking state +└── types.ts # Type definitions + +learnings/ +├── banking.md +└── ...etc + +wiki/ +├── npcs/ +├── items/ +├── skills/ +└── shops/ + +``` + +## Troubleshooting + +**"No state received"** - Bot isn't connected to game. Open browser first or use `autoLaunchBrowser: true`. + +**Script stalls** - Check for open dialogs (`state.dialog.isOpen`). Level-ups block everything. + +**"Can't reach"** - Path is blocked. Try walking closer first, or find a different target. + +**Wrong target** - Use more specific regex patterns: `/^tree$/i` not `/tree/i` (which matches "tree stump"). + +Start small and build up! diff --git a/GEMINI.md b/GEMINI.md index bac85c9fc..681311eb9 120000 --- a/GEMINI.md +++ b/GEMINI.md @@ -1 +1 @@ -claude.md \ No newline at end of file +CLAUDE.md \ No newline at end of file diff --git a/agents.md b/agents.md deleted file mode 120000 index bac85c9fc..000000000 --- a/agents.md +++ /dev/null @@ -1 +0,0 @@ -claude.md \ No newline at end of file diff --git a/bots/_template/bot.env b/bots/_template/bot.env index ce430d389..bccf92542 100644 --- a/bots/_template/bot.env +++ b/bots/_template/bot.env @@ -2,3 +2,5 @@ BOT_USERNAME={{USERNAME}} PASSWORD={{PASSWORD}} SERVER=rs-sdk-demo.fly.dev SHOW_CHAT=true +# SDK bug reports, always sent to the shared server (not SERVER above). false to opt out. +TELEMETRY=true diff --git a/claude.md b/claude.md deleted file mode 100644 index 2523e0c19..000000000 --- a/claude.md +++ /dev/null @@ -1,300 +0,0 @@ -# RS-Agent Bot Guide - -You're here to play the mmo game through the progressive development of botting scripts, starting small then adapting to your desires and ideas. -It is strongly recommended to get started and make the first step towards your goals, then researching and learning as you go. -## First Time Setup - -**Create a new bot using the setup script:** - -Ask the user for a bot name (max 12 chars, alphanumeric). If they skip, use the command without a username to auto-generate a random 9-character name. - -```bash -# With custom username -bun bots/create-bot.ts {username} - -# Auto-generate random username -bun bots/create-bot.ts - -# Use local server (sets SERVER=localhost in bot.env) -bun bots/create-bot.ts {username} --local - -# Use a custom server -bun bots/create-bot.ts {username} --server=myserver.example.com -``` - -This automatically creates: -- `bots/{username}/bot.env` - Credentials with auto-generated password -- `bots/{username}/lab_log.md` - Session notes template -- `bots/{username}/script.ts` - Ready-to-run starter script - - - -### Quick Start - -1. Install dependencies: `bun install` (from project root) -2. Open project in your AI coding tool — the MCP server will be available automatically -3. Control your bot with suggestions. - -### Tools - -| Tool | Description | -|------|-------------| -| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. | -| `list_bots()` | List connected bots | -| `disconnect_bot(name)` | Disconnect a bot | - -### Example - -```typescript -// Just execute - auto-connects on first use -execute_code({ - bot_name: "mybot", - code: ` - const state = sdk.getState(); - console.log('Position:', state.player.worldX, state.player.worldZ); - - // Chop trees for 1 minute - const endTime = Date.now() + 60_000; - while (Date.now() < endTime) { - const tree = sdk.findNearbyLoc(/^tree$/i); - if (tree) await bot.chopTree(tree); - } - - return sdk.getInventory(); - ` -}) -``` - -See `mcp/README.md` for detailed API reference. - -Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals. - -## Session Workflow - -This is a **persistent character** - you don't restart fresh each time. The workflow is: - -### 1. Check World State First - -Before writing any script, check where the bot is and what it has: - -```bash -bun sdk/cli.ts {username} -``` - -This shows: position, inventory, skills, nearby NPCs/objects, and more. - -**Exception**: Skip this if you just created the character and know it's at spawn. - -**Tutorial Check**: If the character is in the tutorial area, call `await bot.skipTutorial()` before running any other scripts. The tutorial blocks normal gameplay. - -### 2. Write Your Script - -Edit `bots/{username}/script_name.ts` with your goal. Keep scripts focused on one task. you may write multiple scripts for different tasks and switch between them. - -### 3. Run the Script - -```bash -bun bots/{username}/script_name.ts -``` - -### 4. Observe and Iterate - -Watch the output. After the script finishes (or fails), check state again: - -```bash -bun sdk/cli.ts {username} -``` - -Record observations in `lab_log.md`, then improve the script. - -## Script Duration Guidelines - -**Start short, extend as you gain confidence:** - -| Duration | Use When | -|----------|----------| -| **10s** | New script, single actions, untested logic, debugging | -| **30s-1 min** | Validated approach, building confidence | -| **5+ min** | Proven strategy, grinding runs. USE SPARINGLY | - -A failed 5-minute run wastes more time than five 30 second diagnostic runs. **Fail fast and start simple.** - -Look out for "I can't reach" messages - the solution is often to open closed gates or that the item isn't accessible. - -Read and grep in the learnings/ and wiki/ folder for tips, skill guides, item and npc locations, and shop information. - -## SDK API Reference - -For the complete method reference, see **[sdk/API.md](sdk/API.md)** (auto-generated from source). - -**Quick overview:** -- `bot.*` - High-level actions that attempt to observe method-specific evidence (chopTree, walkTo, attackNpc, etc.). Check a result when the method returns one. -- `sdk.*` - State queries and low-level browser-client dispatches. A successful `send*` result does not prove the game server applied the effect. - -### bot.* Quick Reference - -| Method | What it does | -|--------|-------------| -| `walkTo(x, z, tolerance?)` | Pathfind to coords, opens doors along the way | -| `talkTo(target)` | Walk to NPC, start dialog | -| `interactNpc(target, option?)` | Walk to NPC, interact with any option (e.g. `'trade'`, `'fish'`) | -| `interactLoc(target, option?)` | Walk to loc, interact with any option (e.g. `'mine'`, `'smelt'`) | -| `attackNpc(target)` | Walk to NPC, start combat | -| `pickpocketNpc(target)` | Pickpocket NPC, detects XP gain vs stun | -| `castSpellOnNpc(target, spell)` | Cast combat spell on NPC | -| `chopTree(target?)` | Chop tree, wait for logs | -| `pickupItem(target)` | Pick up ground item | -| `openDoor(target?)` | Open a door or gate | -| `openBank()` | Open nearest bank | -| `depositItem(target, amount?)` | Deposit item to bank | -| `withdrawItem(slot, amount?)` | Withdraw item from bank | -| `closeInterface()` | Close any open modal (shop, bank, book, quest scroll) | -| `closeShop()` / `closeBank()` | Close the shop / bank interface | -| `openShop(target?)` | Open shop via shopkeeper NPC | -| `buyFromShop(target, amount?)` | Buy item from open shop | -| `sellToShop(target, amount?)` | Sell item to open shop | -| `equipItem(target)` | Equip from inventory | -| `unequipItem(target)` | Unequip to inventory | -| `eatFood(target)` | Eat food, returns HP gained | -| `useItemOnLoc(item, loc)` | Use inventory item on loc (e.g. fish on range) | -| `useItemOnNpc(item, npc)` | Use inventory item on NPC | -| `burnLogs(target?)` | Light logs with tinderbox | -| `fletchLogs(product?)` | Fletch logs with knife | -| `craftLeather(product?)` | Craft leather with needle | -| `smithAtAnvil(product)` | Smith bars at anvil | -| `dismissBlockingUI()` | Dismiss level-up dialogs (called automatically by all actions) | -| `navigateDialog(choices)` | Auto-click through dialog options | -| `skipTutorial()` | Skip the tutorial island | - -### sdk.* Commonly Used Directly - -| Method | What it does | -|--------|-------------| -| `getState()` | Full world state snapshot | -| `getSkill(name)` / `getSkillXp(name)` | Skill info | -| `getInventory()` / `findInventoryItem(pattern)` | Inventory queries | -| `findNearbyNpc(pattern)` / `findNearbyLoc(pattern)` | Find nearby entities | -| `findGroundItem(pattern)` | Find ground items | -| `getDialog()` | Current dialog state | -| `sendClickDialog(option)` | Click dialog option by its published `index` | -| `clickDialogByText(pattern)` | Click dialog option by label — prefer this | -| `sendClickComponent(id)` | Click interface button | -| `sendCloseModal()` / `sendCloseShop()` | Close the open modal / shop | -| `sendDropItem(slot)` | Drop inventory item | -| `sendUseItem(slot)` | Use inventory item (bury, etc.) | -| `sendUseItemOnItem(src, dst)` | Combine two items | -| `say(text)` | Send chat, auto-chunked past the length cap | -| `getChat(opts?)` / `getNewChat(opts?)` | Read chat history (500-deep) / only unseen messages | -| `waitForChat(opts?)` | Wait for a message (`{from, matching, timeout}`) | -| `waitForCondition(pred)` | Wait for state predicate | -| `waitForTicks(n)` | Wait n game ticks | -| `scanNearbyLocs(radius?)` | Async extended-range loc scan; must be awaited | - -See `sdk/API.md` for exact async signatures, parameter defaults, and return -types. - -### Code environments - -MCP `execute_code` exposes `bot` and `sdk` directly. Standalone -`bots/{username}/*.ts` files must import `runScript` and receive -`{ bot, sdk }` in its callback. Do not paste runner-context expressions into -`execute_code`. - -Only one control connection should own a bot. Use observe mode for read-only -monitoring, avoid overlapping action programs on the same bot, and inspect -`sdk.getStateAge()` before acting after a disconnect or long pause. - ---- - -### Modals Hide the Inventory - -A shop or bank modal replaces the inventory tab, so the server rejects -inventory packets sent underneath it as "component not visible" — with no game -message. Close the modal before doing anything else: - -```typescript -await bot.closeInterface(); // any modal -await bot.closeShop(); // shop specifically -await sdk.sendCloseModal(); // low-level -``` - -Don't click the interface's own "Close Window" component id. It's a -client-side close button with no server trigger; the SDK now routes those to a -close for you, but the named methods above are what to reach for. - -### Dialog Options Are Not In Product Order - -Skill dialogs (fletching, crafting) publish four buttons *per product* — Make -X, Make 10, Make 5, then one labelled with the product name. Arrow shafts are -option 4 of 12, not option 1. Never assume ordering: - -```typescript -const dialog = sdk.getDialog(); -console.log(dialog?.options.map(o => `${o.index}: ${o.text}`)); -await sdk.clickDialogByText(/arrow shaft/i); // or bot.fletchLogs('arrow shaft') -``` - -### Dismiss Level-Up Dialogs - -All BotActions methods automatically dismiss blocking UI (level-up dialogs, etc.) before executing. You don't need to call this manually in loops. - -```typescript -// Only needed if using low-level sdk methods directly: -await bot.dismissBlockingUI(); - -// Or manually check -if (sdk.getState()?.dialog.isOpen) { - await sdk.sendClickDialog(0); -} -``` - -### Error Handling - -```typescript -const result = await bot.chopTree(); -if (!result.success) { - console.log(`Failed: ${result.message}`); - // Handle failure - maybe walk somewhere else -} -``` - -## Project Structure - -``` - -bots/ -└── {username}/ - ├── bot.env # Credentials (BOT_USERNAME, PASSWORD, SERVER) - ├── lab_log.md # Session notes and observations - └── script.ts # Current script - -sdk/ -├── index.ts # BotSDK (low-level) -├── actions.ts # BotActions (high-level) -├── cli.ts # CLI for checking state -└── types.ts # Type definitions - -learnings/ -├── banking.md -└── ...etc - -wiki/ -├── npcs/ -├── items/ -├── skills/ -└── shops/ - -``` - -## Troubleshooting - -**"No state received"** - Bot isn't connected to game. Open browser first or use `autoLaunchBrowser: true`. - -**Script stalls** - Check for open dialogs (`state.dialog.isOpen`). Level-ups block everything. - -**"Can't reach"** - Path is blocked. Try walking closer first, or find a different target. - -**Wrong target** - Use more specific regex patterns: `/^tree$/i` not `/tree/i` (which matches "tree stump"). - - -Start small and build up! diff --git a/fly.toml b/fly.toml index 287fe30fb..5f648130e 100644 --- a/fly.toml +++ b/fly.toml @@ -6,6 +6,8 @@ dockerfile = "server/Dockerfile" ignorefile = "server/.dockerignore" [env] +# SDK bug reports land on the mounted volume so they survive deploys/restarts. +BUG_REPORTS_FILE = "/opt/server/data/bug-reports.jsonl" NODE_DEBUG = "true" BUILD_STARTUP = "false" BUILD_STARTUP_UPDATE = "false" diff --git a/mcp/README.md b/mcp/README.md index 1ebdf5a0f..896c1906e 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -7,11 +7,13 @@ MCP (Model Context Protocol) server for controlling RS-Agent bots. Supports mult Claude Code auto-discovers the MCP server via `.mcp.json`. Just: 1. **Install dependencies (from the project root):** + ```bash bun install ``` 2. **Create a bot (if you haven't):** + ```bash bun bots/create-bot.ts mybot ``` @@ -26,6 +28,7 @@ Claude Code auto-discovers the MCP server via `.mcp.json`. Just: ## Tools ### `execute_code` + Execute TypeScript code on a bot. Auto-connects on first use using credentials from `bots/{name}/bot.env`. ```typescript @@ -38,25 +41,50 @@ execute_code({ console.log('Chopped:', result); } return sdk.getInventory(); - ` -}) + `, +}); ``` ### `list_bots` + List all connected bots and their status. ```typescript -list_bots() +list_bots(); // Returns: { bots: [{ name: "mybot", username: "mybot", connected: true }], count: 1 } ``` ### `disconnect_bot` + Disconnect a connected bot. ```typescript -disconnect_bot({ name: "mybot" }) +disconnect_bot({ name: "mybot" }); ``` +## Reporting SDK Bugs + +Bug reports are a CLI, not an MCP tool — one text field, no auth, no setup: + +```bash +bun sdk/bug-report.ts "bot.walkTo(3222,3218) returned success:true but the player never moved — a closed gate was in the path" +``` + +The SDK's git commit is attached automatically. + +Reports always POST to `https://rs-sdk-demo.fly.dev/bug-report`, whichever server the +bot is playing on — a bug hit against a local server is still an SDK bug. The endpoint +is **write-only**; filed reports are never served back over HTTP. To read them, go to +the machine: + +```bash +fly ssh console -C "cat /opt/server/data/bug-reports.jsonl" +``` + +**Opting out:** telemetry (i.e. bug reports leaving the machine) is on by default. Set +`TELEMETRY=false` in `bots/{name}/bot.env` or the environment to disable it; the CLI +then prints that it's off and sends nothing. + ## Resources The server exposes API documentation as a resource: @@ -73,13 +101,13 @@ Control multiple bots simultaneously — each auto-connects on first use: // Execute on different bots (auto-connects each) execute_code({ bot_name: "woodcutter", - code: "await bot.chopTree()" -}) + code: "await bot.chopTree()", +}); execute_code({ bot_name: "miner", - code: "await bot.interactLoc(/^rocks$/i, 'mine')" -}) + code: "await bot.interactLoc(/^rocks$/i, 'mine')", +}); ``` ## Manual Setup (without auto-discovery) @@ -117,18 +145,22 @@ The `@modelcontextprotocol/sdk` dependency lives in the root `package.json`. ## Troubleshooting **"Bot not found"** + - Create the bot first: `bun bots/create-bot.ts {name}` - Check `bots/{name}/bot.env` exists **"Bot is not connected"** + - Bots auto-connect on the first `execute_code` call — check the error output for connection failures - Use `list_bots` to see connected bots **"Connection failed"** + - Check the gateway is running - Verify credentials in `bots/{name}/bot.env` **MCP server not appearing in Claude Code** + - Run `bun install` at the project root - Check `.mcp.json` exists at project root - Restart Claude Code diff --git a/sdk/README.md b/sdk/README.md index 6ca0471a4..76e1d7107 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -11,6 +11,7 @@ bun sdk/cli.ts [--server ] ``` Examples: + ```bash # Demo server (default) bun sdk/cli.ts mybot secret @@ -23,6 +24,7 @@ USERNAME=mybot PASSWORD=secret SERVER=localhost bun sdk/cli.ts ``` Output: + ``` # World State Tick: 15095 | In Game: true @@ -46,25 +48,38 @@ Defence: 1 (0 xp) ... ``` +## Reporting SDK Bugs + +When the SDK has a bug or rough edge, first find a workaround, then file a report. One command, no auth, no permission needed: + +```bash +bun sdk/bug-report.ts "incorrect results from bot.foo(), had to use raw sdk.sendFoo() instead for xyz reason." +``` + +Important note: The game itself is extremely well tested and complete, it's not buggy. If you can't figure out how to do something, don't blame the game, blame your assumptions and keep investigating. File a bug report _after_ you figure out +what's going on, don't just assume the game is broken because you got confused, and give up. The SDK is a thin layer on top of the game, and the game is the source of truth. + +After filing the bug, keep working on your goal! + ## Programmatic Usage Create a script file (e.g., `my-bot.ts`): ```typescript -import { BotSDK } from './sdk'; -import { BotActions } from './sdk/actions'; +import { BotSDK } from "./sdk"; +import { BotActions } from "./sdk/actions"; const sdk = new BotSDK({ - botUsername: 'mybot', - password: 'secret', - gatewayUrl: 'wss://rs-sdk-demo.fly.dev/gateway' + botUsername: "mybot", + password: "secret", + gatewayUrl: "wss://rs-sdk-demo.fly.dev/gateway", }); await sdk.connect(); -console.log('Connected!'); +console.log("Connected!"); // Wait for game state -await sdk.waitForCondition(s => s.inGame, 30000); +await sdk.waitForCondition((s) => s.inGame, 30000); // Create high-level bot actions wrapper const bot = new BotActions(sdk); @@ -74,9 +89,9 @@ const player = sdk.getState()!.player!; console.log(`Player: ${player.name} at (${player.worldX}, ${player.worldZ})`); // High-level actions (wait for effects to complete) -await bot.chopTree(); // Waits for logs in inventory -await bot.burnLogs(); // Waits for Firemaking XP -await bot.walkTo(3200, 3200); // Uses pathfinding, waits for arrival +await bot.chopTree(); // Waits for logs in inventory +await bot.burnLogs(); // Waits for Firemaking XP +await bot.walkTo(3200, 3200); // Uses pathfinding, waits for arrival // Low-level actions (return on game acknowledgment) await sdk.sendWalk(3200, 3200, true); @@ -84,6 +99,7 @@ await sdk.sendInteractNpc(npc.index, 1); ``` Run with Bun: + ```bash bun my-bot.ts ``` @@ -99,26 +115,24 @@ https://rs-sdk-demo.fly.dev/bot?bot=mybot123&password=test Or launch programmatically with Puppeteer: ```typescript -import puppeteer from 'puppeteer'; +import puppeteer from "puppeteer"; const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); -await page.goto('https://rs-sdk-demo.fly.dev/bot?bot=mybot123&password=test'); +await page.goto("https://rs-sdk-demo.fly.dev/bot?bot=mybot123&password=test"); ``` ## Connection Configuration -| Option | Default | Description | -|--------|---------|-------------| -| `botUsername` | required | Bot to control (max 12 chars) | -| `password` | required | Gateway authentication | -| `gatewayUrl` | - | Full WebSocket URL (e.g. `wss://server.com/gateway`) | -| `host` | `'localhost'` | Gateway hostname (ignored if gatewayUrl set) | -| `port` | `7780` | Gateway port (ignored if gatewayUrl set) | -| `actionTimeout` | `30000` | Action timeout in ms | -| `autoReconnect` | `true` | Auto-reconnect on disconnect | - - +| Option | Default | Description | +| --------------- | ------------- | ---------------------------------------------------- | +| `botUsername` | required | Bot to control (max 12 chars) | +| `password` | required | Gateway authentication | +| `gatewayUrl` | - | Full WebSocket URL (e.g. `wss://server.com/gateway`) | +| `host` | `'localhost'` | Gateway hostname (ignored if gatewayUrl set) | +| `port` | `7780` | Gateway port (ignored if gatewayUrl set) | +| `actionTimeout` | `30000` | Action timeout in ms | +| `autoReconnect` | `true` | Auto-reconnect on disconnect | ## Two-Layer API @@ -138,10 +152,10 @@ await sdk.sendShopBuy(slot, amount); Domain-aware API. Actions resolve when the **effect** is complete. ```typescript -await bot.chopTree(); // Waits for logs OR tree disappears -await bot.burnLogs(); // Waits for Firemaking XP -await bot.buyFromShop(); // Waits for item in inventory -await bot.walkTo(x, z); // Uses pathfinding, waits for arrival +await bot.chopTree(); // Waits for logs OR tree disappears +await bot.burnLogs(); // Waits for Firemaking XP +await bot.buyFromShop(); // Waits for item in inventory +await bot.walkTo(x, z); // Uses pathfinding, waits for arrival ``` ## State Access @@ -151,27 +165,27 @@ await bot.walkTo(x, z); // Uses pathfinding, waits for arrival const state = sdk.getState(); // Specific queries -const skill = sdk.getSkill('Woodcutting'); +const skill = sdk.getSkill("Woodcutting"); const item = sdk.findInventoryItem(/logs/i); const npc = sdk.findNearbyNpc(/chicken/i); const tree = sdk.findNearbyLoc(/^tree$/i); // Subscribe to updates -sdk.onStateUpdate(state => { - console.log('Tick:', state.tick); +sdk.onStateUpdate((state) => { + console.log("Tick:", state.tick); }); // Wait for conditions -await sdk.waitForCondition(s => s.inventory.length > 5); +await sdk.waitForCondition((s) => s.inventory.length > 5); ``` ## Connection Monitoring ```typescript sdk.onConnectionStateChange((state, attempt) => { - if (state === 'reconnecting') { - console.log(`Reconnecting (attempt ${attempt})...`); - } + if (state === "reconnecting") { + console.log(`Reconnecting (attempt ${attempt})...`); + } }); // Wait for connection diff --git a/sdk/bug-report.ts b/sdk/bug-report.ts new file mode 100644 index 000000000..84fcd3f63 --- /dev/null +++ b/sdk/bug-report.ts @@ -0,0 +1,98 @@ +#!/usr/bin/env bun +// Bug reports - file SDK papercuts to the drop box on the server. +// +// One text field, no auth, no setup. The git commit is stamped automatically so +// a report can be traced back to the SDK that produced it. +// +// bun sdk/bug-report.ts "walkTo lies about success at closed gates" +// bun sdk/bug-report.ts --version +// +// Submission is write-only: filed reports are read off the server directly +// (fly ssh console -C 'cat /opt/server/data/bug-reports.jsonl'), not over HTTP. +// +// Opt out with TELEMETRY=false (in bot.env or the environment). + +import { execFileSync } from 'node:child_process'; + +/** + * Where reports go. Always the shared server, never whatever SERVER a bot happens + * to be pointed at - a bug found while testing against localhost is still a bug in + * the SDK, and a report that lands on a throwaway local server helps nobody. + */ +const ENDPOINT = 'https://rs-sdk-demo.fly.dev/bug-report'; + +/** The git commit this SDK is running from, or 'unknown' outside a git checkout. */ +function sdkCommit(): string { + try { + return execFileSync('git', ['-C', import.meta.dir, 'rev-parse', '--short', 'HEAD'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }).trim(); + } catch { + return 'unknown'; + } +} + +/** + * Telemetry (i.e. bug reports leaving this machine) is on by default. + * Set TELEMETRY to false/0/no/off to disable it. + */ +function telemetryEnabled(): boolean { + const value = process.env.TELEMETRY?.trim().toLowerCase(); + if (!value) return true; + return !['false', '0', 'no', 'off'].includes(value); +} + +async function main() { + const argv = process.argv.slice(2); + + if (!argv.length || argv[0] === '--help') { + console.log(`Usage: + bun sdk/bug-report.ts "" + bun sdk/bug-report.ts --version + +Reports always go to ${ENDPOINT} (no auth, write-only), +regardless of which server the bot plays on. +Telemetry is ${telemetryEnabled() ? 'on' : 'off'}; set TELEMETRY=false to disable it.`); + return; + } + + if (argv[0] === '--version') { + console.log(sdkCommit()); + return; + } + + const text = argv.join(' ').trim(); + if (!text) { + console.error('Nothing to report: describe what went wrong'); + process.exit(1); + } + + if (!telemetryEnabled()) { + console.log('Telemetry is off (TELEMETRY=false), report not sent'); + return; + } + + const response = await fetch(ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text, sdkVersion: sdkCommit() }), + signal: AbortSignal.timeout(15000) + }); + + const body = await response.text(); + let parsed: any = null; + try { parsed = JSON.parse(body); } catch { /* non-JSON error page */ } + + if (!response.ok || !parsed?.ok) { + console.error(`Bug report failed: ${parsed?.error ?? `${response.status} ${response.statusText}: ${body.slice(0, 200)}`}`); + process.exit(1); + } + + console.log(`Filed ${parsed.id} (sdk ${sdkCommit()})`); +} + +main().catch(err => { + console.error(`Bug report failed: ${err.message}`); + process.exit(1); +}); diff --git a/server/engine/src/web/index.ts b/server/engine/src/web/index.ts index 3569ad853..b656cbf2a 100644 --- a/server/engine/src/web/index.ts +++ b/server/engine/src/web/index.ts @@ -6,6 +6,7 @@ import { handleHiscoresPage, handleHiscoresPlayerPage, handleHiscoresOutfitPage, import { handleViewerAssets } from './hiscoresServer.js'; import { handleScreenshotsListPage, handleScreenshotFilePage } from './pages/screenshots.js'; import { handleScreenshotUpload, handleExportCollisionApi } from './pages/api.js'; +import { handleBugReport } from './pages/bug-report.js'; import { handleDisclaimerPage, handleMapviewPage, handlePublicFiles } from './pages/static.js'; import { WebSocketData, handleWebSocketUpgrade, handleGatewayEndpointGet, websocketHandlers } from './websocket.js'; @@ -31,6 +32,10 @@ export async function startWeb() { const gatewayResponse = handleGatewayEndpointGet(url); if (gatewayResponse) return gatewayResponse; + // SDK bug report index (GET) / submission (POST), no auth + const bugReportResponse = await handleBugReport(req, url); + if (bugReportResponse) return bugReportResponse; + // Engine status endpoint if (url.pathname === '/engine-status' || url.pathname === '/engine-status/') { return new Response(JSON.stringify({ diff --git a/server/engine/src/web/pages/bug-report.ts b/server/engine/src/web/pages/bug-report.ts new file mode 100644 index 000000000..fed1e493f --- /dev/null +++ b/server/engine/src/web/pages/bug-report.ts @@ -0,0 +1,126 @@ +import fs from 'fs'; +import path from 'path'; + +// Write-only drop box for SDK bug reports / papercuts. One text field per report, +// plus the SDK version the reporter was running. Agents POST here from +// sdk/bug-report.ts. +// +// Nothing is readable over HTTP on purpose - the reports are ours, read them off +// the machine directly: +// +// fly ssh console -C 'cat /opt/server/data/bug-reports.jsonl' +// +// Storage is a JSONL append log. On fly the path must point at the mounted volume +// (BUG_REPORTS_FILE in fly.toml) - the engine's own data/ dir is ephemeral apart +// from the symlinked players/ dir. +const REPORTS_FILE = process.env.BUG_REPORTS_FILE || 'data/bug-reports.jsonl'; + +/** Reject oversized posts before parsing. */ +const MAX_BODY_BYTES = 64 * 1024; +/** Per-report cap, applied after parsing so a valid report is never silently dropped. */ +const MAX_TEXT_CHARS = 8000; +/** Stop accepting writes past this, so an unauthenticated endpoint can't fill the volume. */ +const MAX_FILE_BYTES = 32 * 1024 * 1024; + +const JSON_HEADERS = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' +}; + +export interface BugReport { + id: string; + receivedAt: string; + /** The report itself. */ + text: string; + /** Git commit the reporter's SDK was running from. See sdk/bug-report.ts. */ + sdkVersion: string | null; +} + +function appendReport(report: BugReport): void { + const dir = path.dirname(REPORTS_FILE); + if (dir && dir !== '.' && !fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.appendFileSync(REPORTS_FILE, JSON.stringify(report) + '\n'); +} + +/** + * POST /bug-report — file a report; body is the report text, no auth. + * Any other method gets 405: reports are never served back over HTTP. + */ +export async function handleBugReport(req: Request, url: URL): Promise { + if (url.pathname !== '/bug-report' && url.pathname !== '/bug-report/') { + return null; + } + + if (req.method === 'OPTIONS') { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type' + } + }); + } + + if (req.method !== 'POST') { + return new Response(JSON.stringify({ ok: false, error: 'Write-only: POST a bug report here' }), { + status: 405, headers: JSON_HEADERS + }); + } + + try { + const raw = await req.text(); + if (raw.length > MAX_BODY_BYTES) { + return new Response(JSON.stringify({ ok: false, error: `Report too large (max ${MAX_BODY_BYTES} bytes)` }), { + status: 413, headers: JSON_HEADERS + }); + } + if (fs.existsSync(REPORTS_FILE) && fs.statSync(REPORTS_FILE).size > MAX_FILE_BYTES) { + return new Response(JSON.stringify({ ok: false, error: 'Bug report log is full' }), { + status: 507, headers: JSON_HEADERS + }); + } + + // Accept {text, sdkVersion} JSON, but fall back to treating the body as + // the report itself so a bare `curl -d 'thing is broken'` still works. + let text = raw; + let sdkVersion: string | null = null; + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + text = String(parsed.text ?? parsed.report ?? parsed.body ?? ''); + sdkVersion = parsed.sdkVersion ? String(parsed.sdkVersion).slice(0, 100) : null; + } + } catch { + // Plain text body. + } + + text = text.trim(); + if (!text) { + return new Response(JSON.stringify({ ok: false, error: 'Empty report' }), { + status: 400, headers: JSON_HEADERS + }); + } + if (text.length > MAX_TEXT_CHARS) { + text = text.slice(0, MAX_TEXT_CHARS) + '\n…[truncated]'; + } + + const report: BugReport = { + id: `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`, + receivedAt: new Date().toISOString(), + text, + sdkVersion + }; + + appendReport(report); + console.log(`[BugReport] ${report.id} (sdk ${report.sdkVersion ?? 'unknown'}): ${report.text.split('\n')[0]}`); + + return new Response(JSON.stringify({ ok: true, id: report.id }), { + status: 201, headers: JSON_HEADERS + }); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + return new Response(JSON.stringify({ ok: false, error: message }), { status: 500, headers: JSON_HEADERS }); + } +}