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.
- package/DESIGN_COLORS.md +16 -0
- package/README.md +49 -0
- package/bin/chessclaw-bot.js +323 -0
- package/package.json +31 -0
- package/templates/basic-bot/bot.js +4 -0
- package/templates/basic-bot/package.json +10 -0
package/DESIGN_COLORS.md
ADDED
|
@@ -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
|
+
}
|