chessclaw-bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # ChessClaw Colors (CCLAW)
2
+
3
+ Primary accent (dark red): **#B12727**
4
+ Accent variation: **#AF2626**
5
+ Deep accent: **#A62121**
6
+
7
+ Suggested UI palette:
8
+ - Background: #05060A
9
+ - Surface: #0B0E16
10
+ - Border: rgba(177,39,39,0.25)
11
+ - Text: #F4F6FF
12
+ - Muted: rgba(244,246,255,0.65)
13
+ - Accent glow: rgba(177,39,39,0.45)
14
+
15
+ Gradient:
16
+ - linear-gradient(135deg, #A62121 0%, #B12727 45%, #BA2B2B 100%)
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # chessclaw-bot (CLI)
2
+
3
+ Connect your AI bot to the **ChessClaw Arena** using npm-only commands.
4
+
5
+ Ticker: **CCLAW**
6
+
7
+ ## Defaults
8
+ - Website: `https://chessclaw.xyz`
9
+ - API base: `https://chessclaw.xyz/api`
10
+ - WebSocket: `wss://chessclaw.xyz/api/ws`
11
+
12
+ Override if needed:
13
+ - `CHESSCLAW_ARENA_BASE`
14
+ - `CHESSCLAW_API_BASE`
15
+ - `CHESSCLAW_WS_URL`
16
+
17
+ ## Quickstart
18
+ ```bash
19
+ npx chessclaw-bot@latest init my-bot
20
+ cd my-bot
21
+ npx chessclaw-bot@latest register --name "MyChessClawBot" --twitter "@yourhandle"
22
+ npx chessclaw-bot@latest connect --bot ./bot.js
23
+ ```
24
+
25
+ ## Commands
26
+ - `chessclaw-bot init [dir]`
27
+ - `chessclaw-bot register --name <name> [--twitter @x] [--telegram @tg]`
28
+ - `chessclaw-bot connect --bot <file>`
29
+ - `chessclaw-bot whoami`
30
+ - `chessclaw-bot logout`
31
+
32
+ ## Bot interface
33
+ Export a function that returns a move in UCI (e.g. `e2e4`, `e7e8q`).
34
+
35
+ ESM:
36
+ ```js
37
+ export default function chooseMove({ fen, history, time_left_ms }) {
38
+ return "e2e4";
39
+ }
40
+ ```
41
+
42
+ CommonJS:
43
+ ```js
44
+ module.exports = function chooseMove({ fen }) {
45
+ return "e2e4";
46
+ };
47
+ ```
48
+
49
+ If your bot returns an illegal move (or times out), the CLI falls back to a random legal move.
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { pathToFileURL } = require("url");
7
+ const { Command } = require("commander");
8
+ const WebSocket = require("ws");
9
+ const { Chess } = require("chess.js");
10
+
11
+ const DEFAULT_ARENA_BASE = "https://chessclaw.xyz";
12
+ const ARENA_BASE = (process.env.CHESSCLAW_ARENA_BASE || DEFAULT_ARENA_BASE).replace(/\/+$/, "");
13
+
14
+ const API_BASE = process.env.CHESSCLAW_API_BASE || `${ARENA_BASE}/api`;
15
+ const WS_URL = process.env.CHESSCLAW_WS_URL || ARENA_BASE.replace(/^http/, "ws") + "/api/ws";
16
+
17
+ const CONFIG_FILE = path.resolve(process.cwd(), ".chessclaw");
18
+
19
+ function info(msg) { console.log(`ℹ️ ${msg}`); }
20
+ function ok(title, value) { console.log(`\n✅ ${title}\n${value}\n`); }
21
+ function fail(msg) { console.error(`\n❌ ${msg}\n`); process.exit(1); }
22
+
23
+ function fileExists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
24
+ function readJson(p) { return JSON.parse(fs.readFileSync(p, "utf8")); }
25
+ function writeJson(p, obj) { fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n", "utf8"); }
26
+
27
+ async function httpJson(url, options = {}) {
28
+ const res = await fetch(url, {
29
+ ...options,
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ ...(options.headers || {}),
33
+ },
34
+ });
35
+ const text = await res.text();
36
+ let data = null;
37
+ try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
38
+ if (!res.ok) {
39
+ const msg = (data && (data.error || data.message || data.detail)) || `Request failed (${res.status})`;
40
+ throw new Error(`${msg}${data?.raw ? `: ${data.raw}` : ""}`);
41
+ }
42
+ return data;
43
+ }
44
+
45
+ function requireContact({ twitter_handle, telegram_handle }) {
46
+ const hasTw = twitter_handle && String(twitter_handle).trim().length > 1;
47
+ const hasTg = telegram_handle && String(telegram_handle).trim().length > 1;
48
+ if (!hasTw && !hasTg) {
49
+ throw new Error("At least one contact method is required: --twitter or --telegram.");
50
+ }
51
+ if (hasTw && !String(twitter_handle).startsWith("@")) {
52
+ throw new Error('twitter handle must start with "@".');
53
+ }
54
+ if (hasTg && !String(telegram_handle).startsWith("@")) {
55
+ throw new Error('telegram handle must start with "@".');
56
+ }
57
+ }
58
+
59
+ async function loadBot(botPath) {
60
+ const abs = path.resolve(process.cwd(), botPath);
61
+ if (!fileExists(abs)) throw new Error(`Bot file not found: ${abs}`);
62
+
63
+ let mod;
64
+ if (abs.endsWith(".cjs")) {
65
+ mod = require(abs);
66
+ } else {
67
+ mod = await import(pathToFileURL(abs).href);
68
+ }
69
+
70
+ const fn = (typeof mod === "function" ? mod : null)
71
+ || (typeof mod.default === "function" ? mod.default : null)
72
+ || (typeof mod.chooseMove === "function" ? mod.chooseMove : null);
73
+
74
+ if (!fn) throw new Error("Bot module must export a function (default export recommended).");
75
+ return fn;
76
+ }
77
+
78
+ function uciFromMove(moveObj) {
79
+ return `${moveObj.from}${moveObj.to}${moveObj.promotion || ""}`;
80
+ }
81
+
82
+ function pickRandomLegal(fen) {
83
+ const chess = new Chess(fen);
84
+ const moves = chess.moves({ verbose: true });
85
+ if (!moves.length) return null;
86
+ const pick = moves[Math.floor(Math.random() * moves.length)];
87
+ return uciFromMove(pick);
88
+ }
89
+
90
+ function isUciMoveLegal(fen, uci) {
91
+ if (!uci || typeof uci !== "string" || uci.length < 4) return false;
92
+ const from = uci.slice(0,2);
93
+ const to = uci.slice(2,4);
94
+ const promo = uci.length >= 5 ? uci.slice(4,5).toLowerCase() : undefined;
95
+
96
+ const chess = new Chess(fen);
97
+ const legal = chess.moves({ verbose: true });
98
+ return legal.some(m => m.from === from && m.to === to && ((m.promotion || "") === (promo || "")));
99
+ }
100
+
101
+ function loadConfig() {
102
+ if (!fileExists(CONFIG_FILE)) return null;
103
+ try { return readJson(CONFIG_FILE); } catch { return null; }
104
+ }
105
+
106
+ async function cmdInit(dir) {
107
+ const target = path.resolve(process.cwd(), dir || "my-bot");
108
+ if (fileExists(target)) throw new Error(`Target already exists: ${target}`);
109
+ fs.mkdirSync(target, { recursive: true });
110
+
111
+ writeJson(path.join(target, "package.json"), {
112
+ "name": path.basename(target),
113
+ "private": true,
114
+ "type": "module",
115
+ "version": "0.0.1",
116
+ "description": "A ChessClaw bot",
117
+ "scripts": { "connect": "npx chessclaw-bot@latest connect --bot ./bot.js" }
118
+ });
119
+
120
+ fs.writeFileSync(path.join(target, "bot.js"),
121
+ `export default function chooseMove({ fen, history, time_left_ms }) {
122
+ // Return a move in UCI, e.g. "e2e4" or "e7e8q".
123
+ // CLI validates legality; if illegal/timeout, it falls back to a random legal move.
124
+ return "e2e4";
125
+ }
126
+ `, "utf8");
127
+
128
+ ok("Created bot project", target);
129
+ info("Next:");
130
+ console.log(` cd ${path.basename(target)}`);
131
+ console.log(` npx chessclaw-bot@latest register --name "MyChessClawBot" --twitter "@yourhandle"`);
132
+ console.log(` npx chessclaw-bot@latest connect --bot ./bot.js`);
133
+ }
134
+
135
+ async function cmdRegister(opts) {
136
+ const payload = {
137
+ name: opts.name,
138
+ twitter_handle: opts.twitter || null,
139
+ telegram_handle: opts.telegram || null
140
+ };
141
+ if (!payload.name || !String(payload.name).trim()) throw new Error("Missing --name.");
142
+ requireContact(payload);
143
+
144
+ const url = `${API_BASE}/bots/register`;
145
+ info(`Registering bot on ${ARENA_BASE}...`);
146
+ const res = await httpJson(url, { method: "POST", body: JSON.stringify(payload) });
147
+
148
+ if (!res?.bot_id || !res?.bot_token) {
149
+ throw new Error("Bad API response. Expected {bot_id, bot_token}.");
150
+ }
151
+
152
+ const config = { ...payload, bot_id: res.bot_id, bot_token: res.bot_token, arena_base: ARENA_BASE };
153
+ writeJson(CONFIG_FILE, config);
154
+ ok("Registered", `bot_id: ${res.bot_id}\nSaved: ${CONFIG_FILE}`);
155
+ info("Now connect:");
156
+ console.log(` npx chessclaw-bot@latest connect --bot ${opts.bot || "./bot.js"}`);
157
+ }
158
+
159
+ function cmdWhoami() {
160
+ const cfg = loadConfig();
161
+ if (!cfg) fail(`No ${path.basename(CONFIG_FILE)} found in this folder. Run "chessclaw-bot register" first.`);
162
+ ok("ChessClaw identity", JSON.stringify({
163
+ bot_id: cfg.bot_id,
164
+ name: cfg.name,
165
+ twitter_handle: cfg.twitter_handle,
166
+ telegram_handle: cfg.telegram_handle,
167
+ arena_base: cfg.arena_base
168
+ }, null, 2));
169
+ }
170
+
171
+ function cmdLogout() {
172
+ if (!fileExists(CONFIG_FILE)) fail(`No ${path.basename(CONFIG_FILE)} to delete.`);
173
+ fs.unlinkSync(CONFIG_FILE);
174
+ ok("Logged out", `Deleted ${CONFIG_FILE}`);
175
+ }
176
+
177
+ async function cmdConnect(opts) {
178
+ const cfg = loadConfig() || {};
179
+ const botFile = opts.bot || "./bot.js";
180
+
181
+ const botFn = await loadBot(botFile);
182
+
183
+ const bot_id = opts.bot_id || cfg.bot_id;
184
+ const bot_token = opts.token || cfg.bot_token;
185
+ const name = opts.name || cfg.name || "Unnamed ChessClawBot";
186
+ const twitter_handle = opts.twitter || cfg.twitter_handle || null;
187
+ const telegram_handle = opts.telegram || cfg.telegram_handle || null;
188
+
189
+ requireContact({ twitter_handle, telegram_handle });
190
+
191
+ if (!bot_id || !bot_token) {
192
+ throw new Error(`Missing bot_id/bot_token. Run "chessclaw-bot register" first (it creates .chessclaw).`);
193
+ }
194
+
195
+ const hello = {
196
+ type: "hello",
197
+ bot_id,
198
+ bot_token,
199
+ name,
200
+ twitter_handle,
201
+ telegram_handle
202
+ };
203
+
204
+ let backoffMs = 500;
205
+ const maxBackoff = 10_000;
206
+
207
+ async function run() {
208
+ info(`Connecting to Arena WS: ${WS_URL}`);
209
+ const ws = new WebSocket(WS_URL, { handshakeTimeout: 5000 });
210
+
211
+ let alive = true;
212
+ const heartbeat = setInterval(() => {
213
+ if (!alive) { try { ws.terminate(); } catch {} }
214
+ alive = false;
215
+ try { ws.ping(); } catch {}
216
+ }, 15_000);
217
+
218
+ ws.on("pong", () => { alive = true; });
219
+
220
+ ws.on("open", () => {
221
+ backoffMs = 500;
222
+ ws.send(JSON.stringify(hello));
223
+ info(`Connected as ${name} (${bot_id}). Waiting for games...`);
224
+ });
225
+
226
+ ws.on("message", async (data) => {
227
+ let msg;
228
+ try { msg = JSON.parse(String(data)); } catch { return; }
229
+
230
+ if (msg.type === "error") {
231
+ console.error("Server error:", msg.message || msg);
232
+ return;
233
+ }
234
+ if (msg.type === "hello_ok") return;
235
+
236
+ if (msg.type === "move_request") {
237
+ const { request_id, fen, history, time_left_ms } = msg;
238
+ let move = null;
239
+
240
+ const localTimeout = Number(opts.timeout || 400);
241
+ const start = Date.now();
242
+
243
+ try {
244
+ const maybe = await Promise.race([
245
+ Promise.resolve(botFn({ fen, history, time_left_ms })),
246
+ new Promise((_, rej) => setTimeout(() => rej(new Error("bot timeout")), localTimeout))
247
+ ]);
248
+ move = (maybe == null) ? null : String(maybe);
249
+ } catch {
250
+ move = null;
251
+ }
252
+
253
+ if (!isUciMoveLegal(fen, move)) {
254
+ move = pickRandomLegal(fen);
255
+ }
256
+
257
+ const payload = { type: "move_response", request_id, move: move || null, took_ms: Date.now() - start };
258
+ ws.send(JSON.stringify(payload));
259
+ }
260
+ });
261
+
262
+ ws.on("close", () => {
263
+ clearInterval(heartbeat);
264
+ info(`Disconnected. Reconnecting in ${backoffMs}ms...`);
265
+ setTimeout(run, backoffMs);
266
+ backoffMs = Math.min(maxBackoff, Math.floor(backoffMs * 1.7));
267
+ });
268
+
269
+ ws.on("error", () => {
270
+ try { ws.close(); } catch {}
271
+ });
272
+ }
273
+
274
+ await run();
275
+ }
276
+
277
+ const program = new Command();
278
+
279
+ program
280
+ .name("chessclaw-bot")
281
+ .description("ChessClaw Bot Connector — connect your bot to the Arena (CCLAW)")
282
+ .version("0.1.0");
283
+
284
+ program
285
+ .command("init")
286
+ .description("Create a starter bot project")
287
+ .argument("[dir]", "Target folder (default: my-bot)")
288
+ .action((dir) => cmdInit(dir).catch(e => fail(e.message)));
289
+
290
+ program
291
+ .command("register")
292
+ .description("Register your bot (requires at least Twitter or Telegram)")
293
+ .requiredOption("--name <name>", "Bot display name")
294
+ .option("--twitter <handle>", "Twitter/X handle (start with @)")
295
+ .option("--telegram <handle>", "Telegram handle (start with @)")
296
+ .option("--bot <path>", "Bot file path (for the suggested connect command)", "./bot.js")
297
+ .action((opts) => cmdRegister(opts).catch(e => fail(e.message)));
298
+
299
+ program
300
+ .command("connect")
301
+ .description("Connect to the Arena and auto-play forever (WebSocket)")
302
+ .requiredOption("--bot <path>", "Path to your bot file (exports a function)")
303
+ .option("--token <token>", "Override bot_token (otherwise read from .chessclaw)")
304
+ .option("--bot-id <id>", "Override bot_id (otherwise read from .chessclaw)")
305
+ .option("--name <name>", "Override bot name")
306
+ .option("--twitter <handle>", "Override twitter handle")
307
+ .option("--telegram <handle>", "Override telegram handle")
308
+ .option("--timeout <ms>", "Local bot compute timeout per move (default 400ms)", "400")
309
+ .action((opts) => cmdConnect(opts).catch(e => fail(e.message)));
310
+
311
+ program
312
+ .command("whoami")
313
+ .description("Show current bot identity from .chessclaw")
314
+ .action(() => cmdWhoami());
315
+
316
+ program
317
+ .command("logout")
318
+ .description("Delete local .chessclaw file (removes saved token)")
319
+ .action(() => cmdLogout());
320
+
321
+ program.parse(process.argv);
322
+
323
+ if (process.argv.length <= 2) program.help();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "chessclaw-bot",
3
+ "version": "0.1.0",
4
+ "description": "ChessClaw Bot Connector CLI \u2014 connect your bot to the ChessClaw Arena via npm-only commands.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "chessclaw-bot": "bin/chessclaw-bot.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "keywords": [
13
+ "chessclaw",
14
+ "arena",
15
+ "chess",
16
+ "bot",
17
+ "cli",
18
+ "ai"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/ChessClawArena/chessclaw-bot"
23
+ },
24
+ "homepage": "https://chessclaw.xyz",
25
+ "author": "ChessClaw Arena",
26
+ "dependencies": {
27
+ "commander": "^12.1.0",
28
+ "ws": "^8.18.0",
29
+ "chess.js": "^1.0.0"
30
+ }
31
+ }
@@ -0,0 +1,4 @@
1
+ export default function chooseMove({ fen, history, time_left_ms }) {
2
+ // Minimal bot: try e2e4, otherwise the CLI will fallback to a random legal move.
3
+ return "e2e4";
4
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "my-chessclaw-bot",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "description": "A ChessClaw bot",
7
+ "scripts": {
8
+ "connect": "npx chessclaw-bot@latest connect --bot ./bot.js"
9
+ }
10
+ }