feat(sdk): add a bug-report CLI and a write-only endpoint to collect them

Agents hit SDK papercuts constantly - a method returns success:true and
nothing happened, a dialog option isn't where the docs say it is - and
there was nowhere to put that. Filing one is now a single command:

  bun sdk/bug-report.ts "walkTo returns success at closed gates"

The SDK's git commit is stamped on every report, so a report points at the
code that produced it.

Server side, POST /bug-report appends JSONL to the mounted volume
(BUG_REPORTS_FILE in fly.toml, so reports survive deploys). The endpoint is
deliberately write-only: nothing is served back over HTTP, and reports are
read off the machine with

  fly ssh console -C "cat /opt/server/data/bug-reports.jsonl"

Reports always go to rs-sdk-demo.fly.dev, never to whatever SERVER a bot is
playing on - a bug found against a local server is still a bug in the SDK.
Telemetry is on by default; TELEMETRY=false in bot.env or the environment
turns it off.

Also renames claude.md -> CLAUDE.md and agents.md -> AGENTS.md. Those names
only resolved on macOS: the filesystem is case-insensitive here, so git had
recorded lowercase names that no tool would find on a case-sensitive
checkout. Verified on a case-sensitive volume - before the rename, lookups
for CLAUDE.md and AGENTS.md both missed; after, all three resolve to the one
real file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Opus 5 committed 2026-07-25 14:43:49 -04:00
1 parent 344432ed36
commit 11424fa84d
12 files changed
+636 -344

No files matched your search

Symlink
+1
View File
@@ -0,0 +1 @@
CLAUDE.md
+313
View File
@@ -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!
+1 -1
View File
@@ -1 +1 @@
claude.md
CLAUDE.md
-1
View File
@@ -1 +0,0 @@
claude.md
+2
View File
@@ -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
-300
View File
@@ -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!
+2
View File
@@ -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"
+40 -8
View File
@@ -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
+48 -34
View File
@@ -11,6 +11,7 @@ bun sdk/cli.ts <username> <password> [--server <host>]
```
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
+98
View File
@@ -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 "<what went wrong>"
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);
});
+5
View File
@@ -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({
+126
View File
@@ -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<Response | null> {
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 });
}
}