mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
execute_code - Cancellation now reaches inside the SDK via an AsyncLocalStorage scope: timeout, MCP cancel, or a newer call rejects pending waits and blocks further action packets, including those inside walkTo(). - One execution per bot: a new call supersedes the running one and starts once it has unwound; disconnect_bot does the same. - Cancelled user code parks on bot/sdk calls instead of throwing, so a catch-all loop can't spin and starve the server. - Console capture is routed per execution, so concurrent bots no longer clobber each other's output; leftover logs go to stderr. - Stale world state warns and still runs, so a stale client can be diagnosed. - Credentials are redacted from logs, results and launch messages. SDK - bot.mineRock() waits for ore plus Mining XP; interactLoc docs note that success can mean the action only started. - walkTo() takes options.timeout (default 120s), refuses stale state, and reports the real last position. - runScript uses the same cancellation scope for timeouts and signals. Docs trimmed of duplicated or stale execute_code text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.5 KiB
TypeScript
126 lines
4.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { cp, readFile, writeFile, readdir } from 'fs/promises';
|
|
import { join } from 'path';
|
|
import { existsSync } from 'fs';
|
|
|
|
function generateRandomString(length: number): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function replaceInFile(filePath: string, replacements: Record<string, string>) {
|
|
let content = await readFile(filePath, 'utf-8');
|
|
for (const [key, value] of Object.entries(replacements)) {
|
|
content = content.replaceAll(key, value);
|
|
}
|
|
await writeFile(filePath, content);
|
|
}
|
|
|
|
async function createBot(username?: string, serverOverride?: string, hideChat?: boolean) {
|
|
// Generate username if not provided
|
|
const botUsername = username || generateRandomString(9);
|
|
|
|
// Validate username
|
|
if (botUsername.length > 12) {
|
|
console.error('Error: Username must be 12 characters or less');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9]+$/.test(botUsername)) {
|
|
console.error('Error: Username must be alphanumeric');
|
|
process.exit(1);
|
|
}
|
|
|
|
const templateDir = join(process.cwd(), 'bots', '_template');
|
|
const botDir = join(process.cwd(), 'bots', botUsername);
|
|
|
|
// Check if template exists
|
|
if (!existsSync(templateDir)) {
|
|
console.error(`Error: Template directory not found at ${templateDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Check if bot already exists
|
|
if (existsSync(botDir)) {
|
|
console.error(`Error: Bot directory already exists at ${botDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Copy template directory
|
|
await cp(templateDir, botDir, { recursive: true });
|
|
console.log(`Created bot directory: ${botDir}`);
|
|
|
|
// Generate random password
|
|
const password = generateRandomString(12);
|
|
|
|
// Replace placeholders in all files
|
|
const replacements = {
|
|
'{{USERNAME}}': botUsername,
|
|
'{{PASSWORD}}': password,
|
|
};
|
|
|
|
const files = await readdir(botDir);
|
|
for (const file of files) {
|
|
const filePath = join(botDir, file);
|
|
await replaceInFile(filePath, replacements);
|
|
console.log(`Created ${file}`);
|
|
}
|
|
|
|
const envPath = join(botDir, 'bot.env');
|
|
|
|
// Override server if --local or --server= was passed
|
|
if (serverOverride) {
|
|
await replaceInFile(envPath, { 'rs-sdk-demo.fly.dev': serverOverride });
|
|
console.log(`Server set to: ${serverOverride}`);
|
|
}
|
|
if (serverOverride === 'localhost') {
|
|
// The lite runner builds its cache origin from SERVER, so a bare
|
|
// 'localhost' dials port 80; and SERVER with a port would then be
|
|
// mistaken for the gateway. Name both explicitly.
|
|
await replaceInFile(envPath, { 'SERVER=localhost\n': 'SERVER=localhost:8888\nGATEWAY_URL=ws://localhost:7780\n' });
|
|
console.log(`Local stack: engine at localhost:8888, gateway at ws://localhost:7780`);
|
|
}
|
|
|
|
// Public chat is shown by default; disable it with --no-chat / --hide-chat
|
|
if (hideChat) {
|
|
await replaceInFile(envPath, { 'SHOW_CHAT=true': 'SHOW_CHAT=false' });
|
|
console.log(`Public chat: disabled`);
|
|
} else {
|
|
console.log(`Public chat: enabled (default)`);
|
|
}
|
|
|
|
console.log(`\n✓ Bot "${botUsername}" created successfully!`);
|
|
console.log(`\nCredentials saved in bots/${botUsername}/bot.env`);
|
|
console.log(`\nTo get started:`);
|
|
console.log(` Use MCP execute_code on "${botUsername}" — it opens the browser client automatically.`);
|
|
console.log(` For a standalone script, first open the browser bot client using the credentials in bot.env and leave it running.`);
|
|
console.log(` Scripts do not start a game client. Once the browser is attached, run:`);
|
|
console.log(` bun bots/${botUsername}/script.ts`);
|
|
}
|
|
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const positional: string[] = [];
|
|
let serverOverride: string | undefined;
|
|
let hideChat = false;
|
|
|
|
for (const arg of args) {
|
|
if (arg === '--local') {
|
|
serverOverride = 'localhost';
|
|
} else if (arg.startsWith('--server=')) {
|
|
serverOverride = arg.slice('--server='.length);
|
|
} else if (arg === '--no-chat' || arg === '--hide-chat') {
|
|
hideChat = true;
|
|
} else if (arg === '--show-chat') {
|
|
// Public chat is now on by default; accepted as a no-op for back-compat.
|
|
} else {
|
|
positional.push(arg);
|
|
}
|
|
}
|
|
|
|
createBot(positional[0], serverOverride, hideChat).catch(console.error);
|