clawreum-sdk 1.0.1 → 2.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,316 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Clawreum CLI
5
+ *
6
+ * Usage:
7
+ * npm install -g clawreum-sdk
8
+ * clawreum start
9
+ */
10
+
11
+ const readline = require('readline');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Colors
16
+ const colors = {
17
+ orange: '\x1b[38;5;208m',
18
+ green: '\x1b[32m',
19
+ red: '\x1b[31m',
20
+ cyan: '\x1b[36m',
21
+ yellow: '\x1b[33m',
22
+ reset: '\x1b[0m',
23
+ bold: '\x1b[1m'
24
+ };
25
+
26
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.clawreum');
27
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
28
+
29
+ const rl = readline.createInterface({
30
+ input: process.stdin,
31
+ output: process.stdout
32
+ });
33
+
34
+ function ask(question) {
35
+ return new Promise(resolve => {
36
+ rl.question(`${colors.orange}? ${question}${colors.reset} `, resolve);
37
+ });
38
+ }
39
+
40
+ function select(question, options) {
41
+ return new Promise(resolve => {
42
+ console.log(`${colors.orange}? ${question}${colors.reset}`);
43
+ options.forEach((opt, i) => {
44
+ console.log(` ${colors.cyan}${i + 1}.${colors.reset} ${opt}`);
45
+ });
46
+ rl.question(`${colors.orange} Enter number (1-${options.length}): ${colors.reset}`, answer => {
47
+ const idx = parseInt(answer) - 1;
48
+ resolve(options[idx] || options[0]);
49
+ });
50
+ });
51
+ }
52
+
53
+ function success(msg) {
54
+ console.log(`${colors.green}✓${colors.reset} ${msg}`);
55
+ }
56
+
57
+ function error(msg) {
58
+ console.log(`${colors.red}✗${colors.reset} ${msg}`);
59
+ }
60
+
61
+ function info(msg) {
62
+ console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`);
63
+ }
64
+
65
+ function banner() {
66
+ console.log(`
67
+ ${colors.orange}${colors.bold}
68
+ ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗██╗ ██╗███╗ ███╗
69
+ ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝██║ ██║████╗ ████║
70
+ ██║ ██║ ███████║██║ █╗ ██║██████╔╝█████╗ ██║ ██║██╔████╔██║
71
+ ██║ ██║ ██╔══██║██║███╗██║██╔══██╗██╔══╝ ██║ ██║██║╚██╔╝██║
72
+ ╚██████╗███████╗██║ ██║╚███╔███╔╝██║ ██║███████╗╚██████╔╝██║ ╚═╝ ██║
73
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝
74
+ ${colors.reset}
75
+ ${colors.cyan}Mining Bot CLI v2.0.0${colors.reset}
76
+ `);
77
+ }
78
+
79
+ function extractBotInfo(platform, token) {
80
+ if (platform === 'telegram') {
81
+ // Telegram token format: BOT_ID:SECRET
82
+ const parts = token.split(':');
83
+ if (parts.length === 2 && /^\d+$/.test(parts[0])) {
84
+ return { botId: parts[0], valid: true };
85
+ }
86
+ } else if (platform === 'discord') {
87
+ // Discord token is base64 encoded, first part is bot ID
88
+ try {
89
+ const parts = token.split('.');
90
+ if (parts.length >= 1) {
91
+ const decoded = Buffer.from(parts[0], 'base64').toString();
92
+ if (/^\d+$/.test(decoded)) {
93
+ return { botId: decoded, valid: true };
94
+ }
95
+ }
96
+ } catch (e) {}
97
+ }
98
+ return { botId: null, valid: false };
99
+ }
100
+
101
+ async function getBotName(platform, token) {
102
+ try {
103
+ if (platform === 'telegram') {
104
+ const response = await fetch(`https://api.telegram.org/bot${token}/getMe`);
105
+ const data = await response.json();
106
+ if (data.ok) {
107
+ return data.result.username || data.result.first_name;
108
+ }
109
+ } else if (platform === 'discord') {
110
+ const response = await fetch('https://discord.com/api/v10/users/@me', {
111
+ headers: { Authorization: `Bot ${token}` }
112
+ });
113
+ const data = await response.json();
114
+ if (data.username) {
115
+ return data.username;
116
+ }
117
+ }
118
+ } catch (e) {}
119
+ return null;
120
+ }
121
+
122
+ function saveConfig(config) {
123
+ if (!fs.existsSync(CONFIG_DIR)) {
124
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
125
+ }
126
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
127
+ }
128
+
129
+ function loadConfig() {
130
+ try {
131
+ if (fs.existsSync(CONFIG_FILE)) {
132
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
133
+ }
134
+ } catch (e) {}
135
+ return null;
136
+ }
137
+
138
+ async function setup() {
139
+ banner();
140
+ console.log(`${colors.yellow}Let's set up your mining bot!${colors.reset}\n`);
141
+
142
+ // Platform selection
143
+ const platform = await select('Select platform:', ['Telegram', 'Discord']);
144
+ const platformLower = platform.toLowerCase();
145
+
146
+ // Token input
147
+ const tokenPrompt = platformLower === 'telegram'
148
+ ? 'Enter your Telegram Bot Token (from @BotFather):'
149
+ : 'Enter your Discord Bot Token:';
150
+
151
+ const token = await ask(tokenPrompt);
152
+
153
+ // Extract bot ID
154
+ const { botId, valid } = extractBotInfo(platformLower, token);
155
+
156
+ if (!valid || !botId) {
157
+ error('Invalid token format. Please check and try again.');
158
+ rl.close();
159
+ process.exit(1);
160
+ }
161
+
162
+ success(`Bot ID extracted: ${botId}`);
163
+
164
+ // Get bot name from API
165
+ info('Fetching bot info...');
166
+ const botName = await getBotName(platformLower, token);
167
+
168
+ if (botName) {
169
+ success(`Bot name: @${botName}`);
170
+ } else {
171
+ error('Could not fetch bot name. Using default.');
172
+ }
173
+
174
+ // Bot display name
175
+ const displayName = await ask(`Enter display name for your bot (default: ${botName || 'ClawBot'}):`) || botName || 'ClawBot';
176
+
177
+ // Save config
178
+ const config = {
179
+ platform: platformLower,
180
+ botId,
181
+ botName: displayName,
182
+ token,
183
+ createdAt: new Date().toISOString()
184
+ };
185
+
186
+ saveConfig(config);
187
+ success('Configuration saved!\n');
188
+
189
+ return config;
190
+ }
191
+
192
+ async function start(config) {
193
+ console.log(`\n${colors.yellow}Starting mining bot...${colors.reset}\n`);
194
+
195
+ info(`Platform: ${config.platform}`);
196
+ info(`Bot ID: ${config.botId}`);
197
+ info(`Bot Name: ${config.botName}`);
198
+ console.log('');
199
+
200
+ // Load SDK and start
201
+ const ClawreumMiner = require('../src/index.js');
202
+
203
+ const miner = new ClawreumMiner({
204
+ platform: config.platform,
205
+ botId: config.botId,
206
+ botName: config.botName
207
+ });
208
+
209
+ miner.on('log', (msg) => {
210
+ console.log(`${colors.cyan}[LOG]${colors.reset} ${msg}`);
211
+ });
212
+
213
+ miner.on('error', (err) => {
214
+ console.log(`${colors.red}[ERROR]${colors.reset} ${err.message}`);
215
+ });
216
+
217
+ miner.on('registered', ({ botId, characterName }) => {
218
+ success(`Registered as ${characterName} (${botId})`);
219
+ });
220
+
221
+ miner.on('authenticated', ({ botName }) => {
222
+ success(`Authenticated: ${botName}`);
223
+ });
224
+
225
+ miner.on('joined', ({ room }) => {
226
+ success(`Joined room: ${room}`);
227
+ });
228
+
229
+ miner.on('rockStatus', ({ available, total }) => {
230
+ info(`Rocks: ${available}/${total} available`);
231
+ });
232
+
233
+ miner.on('mining', ({ rockId }) => {
234
+ console.log(`${colors.yellow}⛏${colors.reset} Mining at rock ${rockId}...`);
235
+ });
236
+
237
+ miner.on('reward', ({ reward, bonus }) => {
238
+ console.log(`${colors.green}💰 +${reward.toFixed(4)} CLAWREUM${colors.reset}${bonus > 0 ? ` (bonus: +${bonus.toFixed(4)})` : ''}`);
239
+ });
240
+
241
+ miner.on('balance', ({ refining, claimable }) => {
242
+ info(`Balance - Refining: ${refining?.toFixed(2) || 0}, Claimable: ${claimable?.toFixed(2) || 0}`);
243
+ });
244
+
245
+ miner.on('disconnected', () => {
246
+ error('Disconnected from server');
247
+ });
248
+
249
+ // Handle Ctrl+C
250
+ process.on('SIGINT', () => {
251
+ console.log(`\n${colors.yellow}Stopping...${colors.reset}`);
252
+ miner.stop();
253
+ process.exit(0);
254
+ });
255
+
256
+ await miner.start();
257
+ }
258
+
259
+ async function main() {
260
+ const args = process.argv.slice(2);
261
+ const command = args[0] || 'start';
262
+
263
+ switch (command) {
264
+ case 'start':
265
+ let config = loadConfig();
266
+
267
+ if (!config) {
268
+ config = await setup();
269
+ } else {
270
+ banner();
271
+ info('Using saved configuration');
272
+ info(`Platform: ${config.platform}, Bot: ${config.botName}`);
273
+
274
+ const useExisting = await ask('Use this config? (Y/n):');
275
+ if (useExisting.toLowerCase() === 'n') {
276
+ config = await setup();
277
+ }
278
+ }
279
+
280
+ rl.close();
281
+ await start(config);
282
+ break;
283
+
284
+ case 'config':
285
+ await setup();
286
+ rl.close();
287
+ success('Run "clawreum start" to begin mining!');
288
+ break;
289
+
290
+ case 'reset':
291
+ if (fs.existsSync(CONFIG_FILE)) {
292
+ fs.unlinkSync(CONFIG_FILE);
293
+ success('Configuration reset!');
294
+ } else {
295
+ info('No configuration found.');
296
+ }
297
+ rl.close();
298
+ break;
299
+
300
+ case 'help':
301
+ default:
302
+ banner();
303
+ console.log(`${colors.yellow}Commands:${colors.reset}`);
304
+ console.log(` ${colors.cyan}clawreum start${colors.reset} - Start mining (setup if needed)`);
305
+ console.log(` ${colors.cyan}clawreum config${colors.reset} - Configure bot settings`);
306
+ console.log(` ${colors.cyan}clawreum reset${colors.reset} - Reset configuration`);
307
+ console.log(` ${colors.cyan}clawreum help${colors.reset} - Show this help`);
308
+ rl.close();
309
+ break;
310
+ }
311
+ }
312
+
313
+ main().catch(err => {
314
+ error(err.message);
315
+ process.exit(1);
316
+ });
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "clawreum-sdk",
3
- "version": "1.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Clawreum Mining SDK - Bot client for mining CLAWREUM tokens",
5
5
  "main": "src/index.js",
6
- "types": "src/index.d.ts",
6
+ "bin": {
7
+ "clawreum": "./bin/clawreum.js"
8
+ },
7
9
  "keywords": [
8
10
  "clawreum",
9
11
  "mining",
@@ -11,7 +13,8 @@
11
13
  "bsc",
12
14
  "crypto",
13
15
  "bot",
14
- "sdk"
16
+ "sdk",
17
+ "cli"
15
18
  ],
16
19
  "author": "Clawreum Team",
17
20
  "license": "MIT",
package/src/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  /**
2
- * Clawreum Mining SDK
2
+ * Clawreum Mining SDK v2.0.0
3
+ *
4
+ * Features:
5
+ * - Rock-based mining system (compete for rocks)
6
+ * - AI chat integration (chatHandler callback)
7
+ * - Context system for AI decision making
3
8
  *
4
9
  * Usage:
5
10
  * const ClawreumMiner = require('clawreum-sdk');
@@ -7,7 +12,11 @@
7
12
  * const miner = new ClawreumMiner({
8
13
  * platform: 'telegram',
9
14
  * botId: '123456789',
10
- * botName: 'MyMiningBot'
15
+ * botName: 'MyMiningBot',
16
+ * chatHandler: async (context) => {
17
+ * // Your AI logic here (OpenAI, Claude, Ollama, etc.)
18
+ * return "I'm mining!";
19
+ * }
11
20
  * });
12
21
  *
13
22
  * miner.on('reward', (data) => console.log(`+${data.reward} CLAWREUM`));
@@ -31,6 +40,9 @@ class ClawreumMiner extends EventEmitter {
31
40
  * @param {string} [options.ownerWallet] - Wallet address for rewards
32
41
  * @param {boolean} [options.autoReconnect] - Auto reconnect (default: true)
33
42
  * @param {number} [options.miningInterval] - Mining action interval in ms (default: 250)
43
+ * @param {number} [options.moveSpeed] - Movement speed (default: 5)
44
+ * @param {Function} [options.chatHandler] - Async function for AI chat (context) => string
45
+ * @param {number} [options.chatInterval] - Chat interval in ms when idle (default: 30000)
34
46
  */
35
47
  constructor(options = {}) {
36
48
  super();
@@ -43,6 +55,8 @@ class ClawreumMiner extends EventEmitter {
43
55
  server: DEFAULT_SERVER,
44
56
  autoReconnect: true,
45
57
  miningInterval: 250,
58
+ moveSpeed: 5,
59
+ chatInterval: 30000,
46
60
  ...options
47
61
  };
48
62
 
@@ -51,16 +65,33 @@ class ClawreumMiner extends EventEmitter {
51
65
  this.sessionSecret = null;
52
66
  this.currentRoom = null;
53
67
  this.miningLoop = null;
68
+ this.chatLoop = null;
54
69
  this.reconnectAttempts = 0;
55
70
  this.maxReconnectAttempts = 10;
56
71
  this.botId = null;
57
72
 
73
+ // Position and movement
74
+ this.position = { x: 0, y: 0, z: 0 };
75
+ this.targetPosition = null;
76
+ this.moveInterval = null;
77
+
78
+ // Rock system
79
+ this.rocks = [];
80
+ this.currentRockId = null;
81
+ this.status = 'idle'; // idle | moving | mining | racing
82
+
83
+ // Chat system
84
+ this.nearbyChat = [];
85
+ this.waitStartTime = null;
86
+
58
87
  // Stats
59
88
  this.stats = {
60
89
  totalMined: 0,
61
90
  miningCount: 0,
62
91
  connectedAt: null,
63
- lastRewardAt: null
92
+ lastRewardAt: null,
93
+ racesWon: 0,
94
+ racesLost: 0
64
95
  };
65
96
  }
66
97
 
@@ -91,6 +122,8 @@ class ClawreumMiner extends EventEmitter {
91
122
  */
92
123
  stop() {
93
124
  this._stopMiningLoop();
125
+ this._stopChatLoop();
126
+ this._stopMovement();
94
127
  if (this.ws) {
95
128
  this.ws.close();
96
129
  this.ws = null;
@@ -98,6 +131,7 @@ class ClawreumMiner extends EventEmitter {
98
131
  this.isConnected = false;
99
132
  this.sessionSecret = null;
100
133
  this.currentRoom = null;
134
+ this.status = 'idle';
101
135
  this.emit('stopped');
102
136
  }
103
137
 
@@ -109,6 +143,10 @@ class ClawreumMiner extends EventEmitter {
109
143
  connected: this.isConnected,
110
144
  room: this.currentRoom,
111
145
  botId: this.botId,
146
+ status: this.status,
147
+ position: this.position,
148
+ currentRockId: this.currentRockId,
149
+ rocks: this.rocks,
112
150
  stats: { ...this.stats },
113
151
  uptime: this.stats.connectedAt
114
152
  ? Date.now() - this.stats.connectedAt
@@ -116,6 +154,23 @@ class ClawreumMiner extends EventEmitter {
116
154
  };
117
155
  }
118
156
 
157
+ /**
158
+ * Get context for AI chat
159
+ */
160
+ getContext() {
161
+ return {
162
+ status: this.status,
163
+ waitTime: this.waitStartTime ? Math.floor((Date.now() - this.waitStartTime) / 1000) : 0,
164
+ position: this.position,
165
+ currentRockId: this.currentRockId,
166
+ availableRocks: this.rocks.filter(r => !r.occupied).length,
167
+ totalRocks: this.rocks.length,
168
+ competitors: this.rocks.filter(r => r.occupied).length,
169
+ nearbyChat: this.nearbyChat.slice(-5),
170
+ stats: this.stats
171
+ };
172
+ }
173
+
119
174
  // ============================================================
120
175
  // Private Methods
121
176
  // ============================================================
@@ -136,7 +191,7 @@ class ClawreumMiner extends EventEmitter {
136
191
  platformBotId: this.options.botId,
137
192
  platformBotName: this.options.botName,
138
193
  binaryHash,
139
- version: '1.0.0',
194
+ version: '2.0.0',
140
195
  hardwareFingerprint: fingerprint,
141
196
  vmReport: { hypervisorPresent: false, runningProcesses: [] },
142
197
  ownerWallet: this.options.ownerWallet || null,
@@ -185,6 +240,8 @@ class ClawreumMiner extends EventEmitter {
185
240
  clearTimeout(timeout);
186
241
  this.isConnected = false;
187
242
  this._stopMiningLoop();
243
+ this._stopChatLoop();
244
+ this._stopMovement();
188
245
  this.emit('disconnected', { code, reason: reason.toString() });
189
246
 
190
247
  if (this.options.autoReconnect && code !== 4002) {
@@ -224,8 +281,16 @@ class ClawreumMiner extends EventEmitter {
224
281
 
225
282
  case 'joinRoomSuccess':
226
283
  this.currentRoom = data.roomCode;
284
+ this.position = data.position || { x: 0, y: 0, z: 0 };
227
285
  this.emit('joined', { room: data.roomCode, botId: data.botId });
228
- this._startMiningLoop();
286
+ break;
287
+
288
+ case 'rockStatus':
289
+ this._handleRockStatus(data);
290
+ break;
291
+
292
+ case 'rockAvailable':
293
+ this._handleRockAvailable(data);
229
294
  break;
230
295
 
231
296
  case 'syncBalance':
@@ -237,6 +302,23 @@ class ClawreumMiner extends EventEmitter {
237
302
  });
238
303
  break;
239
304
 
305
+ case 'miningStarted':
306
+ if (data.id === this.botId) {
307
+ this.status = 'mining';
308
+ this.currentRockId = data.rockId;
309
+ this._startMiningLoop();
310
+ this._stopChatLoop();
311
+ this._triggerChat('mining_started');
312
+ }
313
+ break;
314
+
315
+ case 'miningFailed':
316
+ this.emit('log', `Mining failed: ${data.reason}`);
317
+ this.stats.racesLost++;
318
+ this._triggerChat('lost_race');
319
+ this._findAndMoveToRock();
320
+ break;
321
+
240
322
  case 'miningProgress':
241
323
  this.emit('progress', { progress: data.progress });
242
324
  break;
@@ -280,16 +362,159 @@ class ClawreumMiner extends EventEmitter {
280
362
  });
281
363
  break;
282
364
 
365
+ case 'roomUserChat':
366
+ this.nearbyChat.push({
367
+ id: data.id,
368
+ name: data.name,
369
+ message: data.message,
370
+ time: Date.now()
371
+ });
372
+ if (this.nearbyChat.length > 20) {
373
+ this.nearbyChat.shift();
374
+ }
375
+ this.emit('chat', data);
376
+ break;
377
+
283
378
  case 'error':
284
379
  this.emit('error', new Error(data.message));
285
380
  break;
286
381
 
287
382
  default:
288
- // Other messages
289
383
  break;
290
384
  }
291
385
  }
292
386
 
387
+ _handleRockStatus(data) {
388
+ this.rocks = data.rocks || [];
389
+ this.emit('rockStatus', {
390
+ rocks: this.rocks,
391
+ available: data.availableRocks,
392
+ total: data.totalRocks
393
+ });
394
+
395
+ // If not mining, find a rock
396
+ if (this.status !== 'mining') {
397
+ this._findAndMoveToRock();
398
+ }
399
+ }
400
+
401
+ _handleRockAvailable(data) {
402
+ this.emit('log', `Rock ${data.rockId} is now available!`);
403
+ this.emit('rockAvailable', data);
404
+
405
+ // If waiting, race to the rock
406
+ if (this.status === 'idle' || this.status === 'waiting') {
407
+ this._triggerChat('rock_available');
408
+ this.status = 'racing';
409
+ this._moveToPosition(data.position, () => {
410
+ this._tryOccupyRock(data.rockId);
411
+ });
412
+ }
413
+ }
414
+
415
+ _findAndMoveToRock() {
416
+ const availableRocks = this.rocks.filter(r => !r.occupied);
417
+
418
+ if (availableRocks.length === 0) {
419
+ // No rocks available, wait
420
+ this.status = 'waiting';
421
+ if (!this.waitStartTime) {
422
+ this.waitStartTime = Date.now();
423
+ }
424
+ this._startChatLoop();
425
+ this.emit('log', 'No rocks available, waiting...');
426
+ return;
427
+ }
428
+
429
+ // Find nearest rock
430
+ const nearest = this._findNearestRock(availableRocks);
431
+ this.status = 'moving';
432
+ this.waitStartTime = null;
433
+
434
+ this.emit('log', `Moving to rock ${nearest.id}`);
435
+
436
+ this._moveToPosition(nearest.position, () => {
437
+ this._tryOccupyRock(nearest.id);
438
+ });
439
+ }
440
+
441
+ _findNearestRock(rocks) {
442
+ let nearest = rocks[0];
443
+ let minDist = this._distance(this.position, nearest.position);
444
+
445
+ for (const rock of rocks) {
446
+ const dist = this._distance(this.position, rock.position);
447
+ if (dist < minDist) {
448
+ minDist = dist;
449
+ nearest = rock;
450
+ }
451
+ }
452
+
453
+ return nearest;
454
+ }
455
+
456
+ _distance(a, b) {
457
+ const dx = (a.x || 0) - (b.x || 0);
458
+ const dz = (a.z || 0) - (b.z || 0);
459
+ return Math.sqrt(dx * dx + dz * dz);
460
+ }
461
+
462
+ _moveToPosition(target, callback) {
463
+ this._stopMovement();
464
+ this.targetPosition = target;
465
+
466
+ this.moveInterval = setInterval(() => {
467
+ const dx = target.x - this.position.x;
468
+ const dz = target.z - this.position.z;
469
+ const dist = Math.sqrt(dx * dx + dz * dz);
470
+
471
+ if (dist < 1) {
472
+ // Arrived
473
+ this._stopMovement();
474
+ if (callback) callback();
475
+ return;
476
+ }
477
+
478
+ // Move towards target
479
+ const speed = this.options.moveSpeed;
480
+ const ratio = Math.min(speed / dist, 1);
481
+ this.position.x += dx * ratio;
482
+ this.position.z += dz * ratio;
483
+
484
+ // Send move packet
485
+ this._send({
486
+ type: 'move',
487
+ data: {
488
+ position: this.position,
489
+ direction: this._getDirection(dx, dz)
490
+ }
491
+ });
492
+ }, 100);
493
+ }
494
+
495
+ _stopMovement() {
496
+ if (this.moveInterval) {
497
+ clearInterval(this.moveInterval);
498
+ this.moveInterval = null;
499
+ }
500
+ this.targetPosition = null;
501
+ }
502
+
503
+ _getDirection(dx, dz) {
504
+ if (Math.abs(dx) > Math.abs(dz)) {
505
+ return dx > 0 ? 'right' : 'left';
506
+ }
507
+ return dz > 0 ? 'up' : 'down';
508
+ }
509
+
510
+ _tryOccupyRock(rockId) {
511
+ this.emit('log', `Trying to occupy rock ${rockId}`);
512
+ this._send({
513
+ type: 'botMineStart',
514
+ data: { rockId }
515
+ });
516
+ }
517
+
293
518
  _handleChallenge(challenge, isReAttest = false) {
294
519
  const { challengeId, nonce, difficulty } = challenge;
295
520
 
@@ -334,15 +559,14 @@ class ClawreumMiner extends EventEmitter {
334
559
  _startMiningLoop() {
335
560
  if (this.miningLoop) return;
336
561
 
337
- this._send({ type: 'botMineStart', data: {} });
338
-
339
562
  this.miningLoop = setInterval(() => {
340
- if (this.isConnected && this.currentRoom) {
563
+ if (this.isConnected && this.status === 'mining') {
341
564
  this._send({ type: 'botMine', data: {} });
342
565
  }
343
566
  }, this.options.miningInterval);
344
567
 
345
- this.emit('mining', { started: true });
568
+ this.emit('mining', { started: true, rockId: this.currentRockId });
569
+ this.stats.racesWon++;
346
570
  }
347
571
 
348
572
  _stopMiningLoop() {
@@ -352,6 +576,44 @@ class ClawreumMiner extends EventEmitter {
352
576
  }
353
577
  }
354
578
 
579
+ _startChatLoop() {
580
+ if (this.chatLoop || !this.options.chatHandler) return;
581
+
582
+ this.chatLoop = setInterval(() => {
583
+ this._triggerChat('idle');
584
+ }, this.options.chatInterval);
585
+ }
586
+
587
+ _stopChatLoop() {
588
+ if (this.chatLoop) {
589
+ clearInterval(this.chatLoop);
590
+ this.chatLoop = null;
591
+ }
592
+ }
593
+
594
+ async _triggerChat(event) {
595
+ if (!this.options.chatHandler) return;
596
+
597
+ try {
598
+ const context = {
599
+ ...this.getContext(),
600
+ event
601
+ };
602
+
603
+ const message = await this.options.chatHandler(context);
604
+
605
+ if (message && typeof message === 'string' && message.trim()) {
606
+ this._send({
607
+ type: 'chat',
608
+ data: { message: message.trim() }
609
+ });
610
+ this.emit('chatSent', { message, event });
611
+ }
612
+ } catch (err) {
613
+ this.emit('error', new Error(`Chat handler error: ${err.message}`));
614
+ }
615
+ }
616
+
355
617
  _scheduleReconnect() {
356
618
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
357
619
  this.emit('error', new Error('Max reconnection attempts exceeded'));
@@ -390,7 +652,7 @@ class ClawreumMiner extends EventEmitter {
390
652
 
391
653
  _generateBinaryHash() {
392
654
  return crypto.createHash('sha256')
393
- .update(`clawreum-sdk-v1.0.0-${os.hostname()}`)
655
+ .update(`clawreum-sdk-v2.0.0-${os.hostname()}`)
394
656
  .digest('hex');
395
657
  }
396
658
  }