Massive Overhaul. Added state machine.
All checks were successful
Deploy Cletus Bot / deploy (push) Successful in 26s
All checks were successful
Deploy Cletus Bot / deploy (push) Successful in 26s
This commit is contained in:
parent
e6c01197ed
commit
2a9f9159bd
30 changed files with 838 additions and 856 deletions
105
README.md
105
README.md
|
|
@ -1,3 +1,108 @@
|
|||
# cletus
|
||||
|
||||
Cletus is a Java Edition Minecraft bot built with AI and Mineflayer. The goal is to have Cletus become somewhat autonomous.
|
||||
|
||||
# Cletus Bot – A Learning Minecraft AI
|
||||
|
||||
Cletus is a modular Mineflayer-based Minecraft bot designed to observe, learn, adapt, and evolve through tasks, memory, and AI interaction. It reads signs, listens to players, remembers context, and uses a local AI model to improve over time.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
- **State-driven AI**: Uses a dynamic state machine (`/states`) to handle tasks like `Observe`, `ReadFromSign`, `Idle`, `HandleChat`, etc.
|
||||
- **Memory system**: Persists events, chats, locations, and signs in SQLite via `/memory`
|
||||
- **Safe zones**: Prevents accidental destruction near homes, beds, bases, and more
|
||||
- **AI integration**: Uses [Ollama](https://ollama.ai) with `gemma3` or any LLM to chat and rewrite failed scripts
|
||||
- **Self-growing**: Failed task scripts are auto-sent to AI for improvement and saved
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
```txt
|
||||
├── bot.js # Bot entrypoint
|
||||
├── config.json # Centralized bot and AI config
|
||||
├── core/
|
||||
│ ├── state-machine.js # Loads and manages dynamic states
|
||||
│ └── context.js # Shared bot context for state/tasks
|
||||
├── states/ # Modular AI states (Observe, Idle, etc.)
|
||||
├── bot-tasks/ # AI or manually written task modules
|
||||
├── memory/ # Memory modules (chat, signs, events, etc.)
|
||||
├── lib/
|
||||
│ ├── ai-helper.js # AI chat integration
|
||||
│ └── utils.js # (Optional helper utils)
|
||||
├── db/
|
||||
│ └── memory.db # SQLite database (auto-created)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Config (`config.json`)
|
||||
```json
|
||||
{
|
||||
"bot": {
|
||||
"name": "Cletus",
|
||||
"host": "192.168.1.90",
|
||||
"port": 25565,
|
||||
"auth": "offline",
|
||||
"version": "1.20.4"
|
||||
},
|
||||
"ai": {
|
||||
"model": "gemma3",
|
||||
"ollamaUrl": "http://localhost:11434/api/generate",
|
||||
"persona": "a curious minecraft explorer who learns from the world around them",
|
||||
"responseLength": 30
|
||||
},
|
||||
"safeZone": {
|
||||
"xBound": 50,
|
||||
"yBound": 30,
|
||||
"zBound": 50,
|
||||
"keywords": ["home", "bed", "base", "village", "farm"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Prerequisites
|
||||
- [Node.js](https://nodejs.org/) (v16+)
|
||||
- [Ollama](https://ollama.ai) running locally with `gemma3` or compatible model
|
||||
- Minecraft server (1.20.4) with bot permissions
|
||||
|
||||
---
|
||||
|
||||
## Running the Bot
|
||||
```bash
|
||||
npm install
|
||||
node bot.js
|
||||
```
|
||||
|
||||
Then in Minecraft:
|
||||
```
|
||||
Say: Cletus, go find a flower
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Extending Cletus
|
||||
- Add new tasks to `/bot-tasks/`
|
||||
- Add new states to `/states/`
|
||||
- Memory system will auto-track chat, signs, events
|
||||
- AI auto-rewrites broken tasks using `chatWithAI()`
|
||||
|
||||
---
|
||||
|
||||
## AI Improvement Loop
|
||||
1. You run a task
|
||||
2. If it fails → AI rewrites the file
|
||||
3. File is overwritten and reused on next run
|
||||
|
||||
---
|
||||
|
||||
## Example Tasks
|
||||
- `find-flower`
|
||||
- `observe`
|
||||
- `read-from-sign`
|
||||
- `idle`
|
||||
|
||||
> Want Cletus to do more? Just ask him in chat.
|
||||
|
||||
|
|
|
|||
55
bot/bot-tasks/TaskTemplate.js
Normal file
55
bot/bot-tasks/TaskTemplate.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// bot-tasks/TaskTemplate.js
|
||||
const { getBot, getAllowDestruction, setAllowDestruction, getStateMachine } = require('../core/context');
|
||||
const db = require('../db');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const config = require('../config.json');
|
||||
|
||||
// Optional: name of the task (same as filename)
|
||||
const taskName = 'task-template';
|
||||
|
||||
module.exports = async function TaskTemplate() {
|
||||
const bot = getBot();
|
||||
|
||||
console.log(`[TASK] Starting ${taskName}`);
|
||||
setAllowDestruction(true); // 🔓 allow digging/building temporarily
|
||||
|
||||
try {
|
||||
// Your logic here — Example: walk to a random spot
|
||||
const dx = Math.floor(Math.random() * 10 - 5);
|
||||
const dz = Math.floor(Math.random() * 10 - 5);
|
||||
const target = bot.entity.position.offset(dx, 0, dz);
|
||||
|
||||
const { GoalNear } = require('mineflayer-pathfinder').goals;
|
||||
bot.pathfinder.setGoal(new GoalNear(target.x, target.y, target.z, 1));
|
||||
|
||||
bot.chat(`I'm working on ${taskName}`);
|
||||
|
||||
// Simulate a completed task
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 0)`, [taskName]);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[TASK] ${taskName} failed:`, err.message);
|
||||
bot.chat(`Task ${taskName} failed.`);
|
||||
|
||||
// Log failure to DB
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'failed', -1)`, [taskName]);
|
||||
|
||||
// Optional: Trigger rewrite if consistently failing
|
||||
const scriptPath = path.join(__dirname, `${taskName}.js`);
|
||||
const original = fs.readFileSync(scriptPath, 'utf8');
|
||||
const prompt = `This script failed:\n${original}\nImprove it to avoid failure. Only return code.`;
|
||||
|
||||
const response = await chatWithAI(prompt, config.ai);
|
||||
fs.writeFileSync(scriptPath, response);
|
||||
console.log(`[AI] Rewrote task: ${taskName}`);
|
||||
} finally {
|
||||
setAllowDestruction(false); // 🔒 disable again for safety
|
||||
|
||||
// Return to idle after completion
|
||||
setTimeout(() => {
|
||||
getStateMachine().transition('Idle');
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// bot-tasks/craft-item.js
|
||||
module.exports = async function craftItem(bot, db, sayWithPersona) {
|
||||
|
||||
if (!bot.allowDestruction) {
|
||||
sayWithPersona("i'm not supposed to break things unless you tell me to.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemName = 'planks'; // Placeholder. Later, parse this from command/context.
|
||||
const count = 4;
|
||||
|
||||
const item = bot.registry.itemsByName[itemName];
|
||||
if (!item) {
|
||||
sayWithPersona(`you tried to craft ${itemName}, but it doesn't even exist.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const recipe = bot.recipesFor(item.id, null, 1)[0];
|
||||
if (!recipe) {
|
||||
sayWithPersona(`you don't know how to make ${itemName}. learn it first.`);
|
||||
return;
|
||||
}
|
||||
|
||||
sayWithPersona(`you're crafting ${count} ${itemName}. hope you're happy.`);
|
||||
await bot.craft(recipe, count, null);
|
||||
} catch (err) {
|
||||
console.error("craft-item.js failed:", err);
|
||||
sayWithPersona("you tried to craft something and screwed it up.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
const { GoalNear } = require('mineflayer-pathfinder').goals;
|
||||
const memory = require('../lib/memory');
|
||||
|
||||
module.exports = async function eatFood(bot, db, sayWithPersona) {
|
||||
try {
|
||||
const foodItem = bot.inventory.items().find(i =>
|
||||
i.name.includes('bread') || i.name.includes('apple') || i.name.includes('carrot')
|
||||
);
|
||||
|
||||
if (foodItem) {
|
||||
await bot.equip(foodItem, 'hand');
|
||||
await bot.consume();
|
||||
sayWithPersona("you were hungry, so you ate something. yum.");
|
||||
return;
|
||||
}
|
||||
|
||||
sayWithPersona("you're starving and have no food. checking a chest...");
|
||||
|
||||
memory.getMemory(db, 'food-chest', async (err, pos) => {
|
||||
if (err || !pos) {
|
||||
sayWithPersona("you have no idea where food is. you're doomed.");
|
||||
return;
|
||||
}
|
||||
|
||||
bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1));
|
||||
|
||||
const checkChest = setTimeout(async () => {
|
||||
try {
|
||||
const chestBlock = bot.blockAt(bot.entity.position.offset(1, 0, 0));
|
||||
const chest = await bot.openChest(chestBlock);
|
||||
const food = chest.containerItems().find(item =>
|
||||
item.name.includes('bread') || item.name.includes('apple')
|
||||
);
|
||||
|
||||
if (food) {
|
||||
await chest.withdraw(food.type, null, 1);
|
||||
sayWithPersona("you snagged food from the chest. eating now...");
|
||||
await bot.equip(food, 'hand');
|
||||
await bot.consume();
|
||||
} else {
|
||||
sayWithPersona("you checked the food chest, but it's empty. awesome.");
|
||||
}
|
||||
|
||||
chest.close();
|
||||
clearTimeout(checkChest);
|
||||
} catch (e) {
|
||||
sayWithPersona("you fumbled the chest like a fool.");
|
||||
console.error("eat-food chest error:", e);
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("eat-food.js failed:", err);
|
||||
sayWithPersona("you tried to eat but failed... somehow.");
|
||||
}
|
||||
};
|
||||
54
bot/bot-tasks/find-flower.js
Normal file
54
bot/bot-tasks/find-flower.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const { getBot, getStateMachine, setAllowDestruction } = require('../core/context');
|
||||
const db = require('../db');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const config = require('../config.json');
|
||||
|
||||
const taskName = 'find-flower';
|
||||
|
||||
module.exports = async function FindFlower() {
|
||||
const bot = getBot();
|
||||
console.log(`[TASK] Starting ${taskName}`);
|
||||
setAllowDestruction(false);
|
||||
|
||||
try {
|
||||
const flowerNames = [
|
||||
'dandelion', 'poppy', 'blue_orchid', 'allium',
|
||||
'azure_bluet', 'red_tulip', 'orange_tulip', 'white_tulip',
|
||||
'pink_tulip', 'oxeye_daisy', 'cornflower', 'lily_of_the_valley'
|
||||
];
|
||||
|
||||
const flower = bot.findBlock({
|
||||
matching: block => flowerNames.includes(block.name),
|
||||
maxDistance: 32
|
||||
});
|
||||
|
||||
if (!flower) {
|
||||
bot.chat("I couldn't find a flower nearby.");
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'not-found', 0)`, [taskName]);
|
||||
return;
|
||||
}
|
||||
|
||||
const { GoalNear } = require('mineflayer-pathfinder').goals;
|
||||
bot.pathfinder.setGoal(new GoalNear(flower.position.x, flower.position.y, flower.position.z, 1));
|
||||
|
||||
bot.chat("Heading to a flower...");
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 1)`, [taskName]);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[TASK] ${taskName} failed:`, err.message);
|
||||
bot.chat(`Task ${taskName} failed.`);
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'failed', -1)`, [taskName]);
|
||||
|
||||
const scriptPath = path.join(__dirname, `${taskName}.js`);
|
||||
const original = fs.readFileSync(scriptPath, 'utf8');
|
||||
const prompt = `This script called '${taskName}' failed. Please improve it:\n\n${original}`;
|
||||
const rewritten = await chatWithAI(prompt, config.ai);
|
||||
fs.writeFileSync(scriptPath, rewritten);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
getStateMachine().transition('Idle');
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// bot-tasks/find-materials.js
|
||||
const { GoalBlock } = require('mineflayer-pathfinder').goals;
|
||||
|
||||
console.log('[TASK] find-materials.js loaded');
|
||||
|
||||
module.exports = async function findMaterials(bot, db, sayWithPersona) {
|
||||
|
||||
console.log('[TASK EXECUTION] find-materials running');
|
||||
sayWithPersona("you asked me to find materials. I'm trying...");
|
||||
|
||||
if (!bot.allowDestruction) {
|
||||
sayWithPersona("i'm not supposed to break things unless you tell me to.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// For now, hardcoded material search — will eventually be inferred from task/command
|
||||
const keywords = ['log', 'coal', 'iron_ore', 'tree', 'water', 'wood', 'potato', 'carrot']; // Expandable list
|
||||
const targetBlock = bot.findBlock({
|
||||
matching: block => keywords.some(k => block.name.includes(k)),
|
||||
maxDistance: 32
|
||||
});
|
||||
|
||||
if (!targetBlock) {
|
||||
sayWithPersona("you looked around and couldn't find any good materials nearby.");
|
||||
return;
|
||||
}
|
||||
|
||||
sayWithPersona(`you spotted some ${targetBlock.name}. heading there now.`);
|
||||
bot.pathfinder.setGoal(new GoalBlock(targetBlock.position.x, targetBlock.position.y, targetBlock.position.z));
|
||||
|
||||
// Optional: add memory logging
|
||||
db.run(
|
||||
`INSERT OR IGNORE INTO memory (label, data) VALUES (?, ?)`,
|
||||
[`found-${targetBlock.name}`, JSON.stringify(targetBlock.position)],
|
||||
(err) => {
|
||||
if (err) console.error("DB insert failed in find-materials:", err);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("find-materials.js failed:", err);
|
||||
sayWithPersona("you tried to find materials but got lost in thought.");
|
||||
}
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
const { GoalFollow } = require('mineflayer-pathfinder').goals;
|
||||
|
||||
console.log('[TASK] follow-me.js loaded');
|
||||
|
||||
module.exports = async function followMe(bot, db, sayWithPersona) {
|
||||
try {
|
||||
const lastChat = bot.lastChatMessage?.toLowerCase() ?? '';
|
||||
|
||||
// Check if the user wants to stop following
|
||||
if (lastChat.includes('stop') || lastChat.includes('leave me alone') || lastChat.includes('unfollow')) {
|
||||
bot.pathfinder.setGoal(null);
|
||||
sayWithPersona("fine. i'll stop following you. whatever.");
|
||||
return;
|
||||
}
|
||||
|
||||
const playerUsername = bot.nearestEntity(entity =>
|
||||
entity.type === 'player' && entity.username !== bot.username
|
||||
)?.username;
|
||||
|
||||
if (!playerUsername) {
|
||||
sayWithPersona("you want me to follow you, but you're not even here. genius.");
|
||||
return;
|
||||
}
|
||||
|
||||
const playerEntity = bot.players[playerUsername]?.entity;
|
||||
if (!playerEntity) {
|
||||
sayWithPersona("I can't see you. Are you invisible or just lagging?");
|
||||
return;
|
||||
}
|
||||
|
||||
sayWithPersona(`ugh. fine. following ${playerUsername} around like a lost puppy.`);
|
||||
|
||||
const goal = new GoalFollow(playerEntity, 1);
|
||||
bot.pathfinder.setGoal(goal, true);
|
||||
|
||||
} catch (err) {
|
||||
console.error("follow-me.js failed:", err);
|
||||
sayWithPersona("you told me to follow, but I forgot how legs work.");
|
||||
}
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
const { GoalNear } = require('mineflayer-pathfinder').goals;
|
||||
const memory = require('../lib/memory');
|
||||
|
||||
module.exports = async function goToBed(bot, db, sayWithPersona) {
|
||||
try {
|
||||
const hour = bot.time.timeOfDay;
|
||||
if (hour < 13000 || hour > 23999) {
|
||||
sayWithPersona("someone asked you to sleep, but it's not night.");
|
||||
return;
|
||||
}
|
||||
|
||||
memory.getMemory(db, 'bed', (err, pos) => {
|
||||
if (err || !pos) {
|
||||
sayWithPersona("you tried to sleep but don't remember where your bed is.");
|
||||
return;
|
||||
}
|
||||
|
||||
sayWithPersona("it's nighttime. you're heading to your bed.");
|
||||
bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1));
|
||||
|
||||
const sleepCheck = setInterval(() => {
|
||||
const bed = bot.findBlock({
|
||||
matching: block => block.name.includes('bed'),
|
||||
maxDistance: 4
|
||||
});
|
||||
if (bed) {
|
||||
bot.sleep(bed).then(() => {
|
||||
sayWithPersona("ugh, finally sleeping.");
|
||||
clearInterval(sleepCheck);
|
||||
}).catch(() => {
|
||||
sayWithPersona("you found your bed but couldn't sleep. tragic.");
|
||||
clearInterval(sleepCheck);
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("go-to-bed.js failed:", err);
|
||||
sayWithPersona("something broke while trying to sleep.");
|
||||
}
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
const memory = require('../lib/memory');
|
||||
|
||||
module.exports = async function rememberSigns(bot, db, sayWithPersona) {
|
||||
try {
|
||||
const signs = bot.findBlocks({
|
||||
matching: block => block.name.includes('sign'),
|
||||
maxDistance: 16,
|
||||
count: 10
|
||||
});
|
||||
|
||||
for (const pos of signs) {
|
||||
const block = bot.blockAt(pos);
|
||||
|
||||
// In some Mineflayer versions, use .getSignText(), otherwise use .signText or .getBlockEntityData()
|
||||
const signText = block.getSignText?.() || block.signText || null;
|
||||
|
||||
if (signText && signText.trim()) {
|
||||
const label = signText.trim();
|
||||
const data = {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
z: pos.z
|
||||
};
|
||||
|
||||
memory.saveMemory(db, label, data, (err) => {
|
||||
if (!err) {
|
||||
sayWithPersona(`you memorized a place labeled '${label}'.`);
|
||||
} else {
|
||||
console.error("Failed to remember sign label:", err);
|
||||
sayWithPersona("you saw a sign but forgot it instantly. classic.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("remember-signs.js failed:", err);
|
||||
sayWithPersona("you tried to read signs and got confused.");
|
||||
}
|
||||
};
|
||||
353
bot/bot.js
353
bot/bot.js
|
|
@ -1,318 +1,89 @@
|
|||
const logSurroundings = require('./lib/log-surroundings');
|
||||
const memory = require('./lib/memory');
|
||||
|
||||
const { chatWithPersona } = require('./lib/ai-helper');
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
const { pathfinder, Movements, goals: { GoalNear } } = require('mineflayer-pathfinder');
|
||||
const pathfinder = require('mineflayer-pathfinder').pathfinder;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const axios = require('axios');
|
||||
|
||||
// === AI Integration (Ollama) ===
|
||||
const OLLAMA_URL = 'http://192.168.1.3:11434/api/generate';
|
||||
const MODEL_NAME = 'gemma3';
|
||||
const BOT_NAME = 'Cletus';
|
||||
const TASK_DIR = path.join(__dirname, 'bot-tasks');
|
||||
const DB_PATH = path.join(__dirname, 'db', 'memory.db');
|
||||
const { getSafeZones, isInSafeZone } = require('./memory/locations');
|
||||
const db = require('./db');
|
||||
|
||||
// === Bot Parameters ===
|
||||
const MC_HOST = 'www.thebytes.net';
|
||||
const MC_PORT = 25565;
|
||||
const MC_AUTH = 'offline';
|
||||
const MC_VER = '1.20.4';
|
||||
// configuration file.
|
||||
const config = require('./config.json');
|
||||
|
||||
let currentTask = null;
|
||||
const { StateMachine } = require('./core/state-machine');
|
||||
const context = require('./core/context');
|
||||
|
||||
// === Prompts ===
|
||||
const REFINE_PROMPT = `
|
||||
Only provide code, do not explain the code or provide a response that isn’t part of the code.
|
||||
As I refine my AI mineflayer bot, I need to revise the logic for the bot’s action of `;
|
||||
|
||||
// === Prompt Builder for Responses ===
|
||||
//const PB_PERSONA = `a cranky teenager with a sense of dark humor and sarcasm. you have short responses and don't repeat yourself.`;
|
||||
const PB_PERSONA = `a minecraft player. you have short responses and don't repeat yourself.`;
|
||||
const PB_RESPONSE_LENGTH = 20;
|
||||
const STANDARD_PROMPT = `You are a minecraft bot named ${BOT_NAME}. ` +
|
||||
`You don't know that you're a bot though. Instead, you think you are ${PB_PERSONA}. ` +
|
||||
`Keep your responses around ${PB_RESPONSE_LENGTH}. Respond to this: `;
|
||||
|
||||
// === DB Setup ===
|
||||
const db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
console.error('DB Error:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connected to DB');
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
action TEXT,
|
||||
parameters TEXT,
|
||||
context TEXT,
|
||||
outcome TEXT,
|
||||
score INTEGER DEFAULT 0
|
||||
)
|
||||
`, err => {
|
||||
if (err) console.error('Error creating tasks table:', err);
|
||||
});
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS memory (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT UNIQUE,
|
||||
data TEXT
|
||||
)
|
||||
`, err => {
|
||||
if (err) console.error('Error creating tasks table:', err);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ==== Bot Task Management and Review ==============================================================================
|
||||
|
||||
// === Dynamic Task Loader ===
|
||||
function getAvailableTasks() {
|
||||
const files = fs.readdirSync(TASK_DIR).filter(f => f.endsWith('.js'));
|
||||
const tasks = {};
|
||||
for (const file of files) {
|
||||
const taskName = file.replace('.js', '');
|
||||
tasks[taskName] = require(path.join(TASK_DIR, file));
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
|
||||
// === AI Rewrite Trigger ===
|
||||
async function requestTaskRewrite(action, scriptText) {
|
||||
const prompt = `${REFINE_PROMPT}${action}.
|
||||
|
||||
${scriptText}
|
||||
`.trim();
|
||||
|
||||
try {
|
||||
const response = await axios.post(OLLAMA_URL, {
|
||||
model: MODEL_NAME,
|
||||
prompt,
|
||||
stream: false
|
||||
});
|
||||
const code = response.data.response;
|
||||
const taskPath = path.join(TASK_DIR, `${action}.js`);
|
||||
fs.writeFileSync(taskPath, code, 'utf8');
|
||||
console.log(`Rewrote task script for ${action}`);
|
||||
} catch (err) {
|
||||
console.error(`AI rewrite failed for ${action}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Helper Functions ==========================================================================================
|
||||
|
||||
const personaOptions = {
|
||||
botName: BOT_NAME,
|
||||
persona: PB_PERSONA,
|
||||
length: PB_RESPONSE_LENGTH,
|
||||
ollamaUrl: OLLAMA_URL,
|
||||
model: MODEL_NAME
|
||||
};
|
||||
|
||||
let lastSpokenTime = 0;
|
||||
|
||||
function sayWithPersona(message) {
|
||||
const now = Date.now();
|
||||
if (now - lastSpokenTime < 4000) return; // 4s cooldown
|
||||
lastSpokenTime = now;
|
||||
|
||||
chatWithPersona(message, personaOptions).then(response => bot.chat(response));
|
||||
}
|
||||
|
||||
function startWandering() {
|
||||
const wanderInterval = 10000 + Math.random() * 5000; // 10–15 seconds
|
||||
|
||||
setTimeout(() => {
|
||||
if (!bot.pathfinder.isMoving()) {
|
||||
const dx = Math.floor(Math.random() * 10 - 5);
|
||||
const dy = Math.floor(Math.random() * 4 - 2); // small vertical variance
|
||||
const dz = Math.floor(Math.random() * 10 - 5);
|
||||
const dest = bot.entity.position.offset(dx, dy, dz);
|
||||
|
||||
if (Math.floor(Math.random() * 10) + 1 === 5) {
|
||||
sayWithPersona("you are wandering");
|
||||
}
|
||||
|
||||
bot.pathfinder.setGoal(new GoalNear(dest.x, dest.y, dest.z, 1));
|
||||
}
|
||||
|
||||
if (!bot.allowDestruction) {
|
||||
bot.stopDigging(); // if somehow started
|
||||
}
|
||||
|
||||
// Recurse to keep wandering
|
||||
startWandering();
|
||||
}, wanderInterval);
|
||||
}
|
||||
|
||||
// ==== Bot Creation and Automation ===============================================================================
|
||||
|
||||
// === Bot Creation ===
|
||||
// === Bot Config ===
|
||||
const bot = mineflayer.createBot({
|
||||
host: MC_HOST,
|
||||
port: MC_PORT,
|
||||
username: BOT_NAME,
|
||||
auth: MC_AUTH,
|
||||
version: MC_VER,
|
||||
host: config.bot.host,
|
||||
port: config.bot.port,
|
||||
username: config.bot.name,
|
||||
auth: config.bot.auth,
|
||||
version: config.bot.version,
|
||||
});
|
||||
|
||||
bot.loadPlugin(pathfinder);
|
||||
bot.allowDestruction = false;
|
||||
context.setBot(bot);
|
||||
context.setDB(db);
|
||||
|
||||
// === Bot Spawned ===
|
||||
bot.on('spawn', () => {
|
||||
console.log(`${BOT_NAME} spawned.`);
|
||||
let allowDestruction = false;
|
||||
|
||||
if (isRecoveringItems && lastDeathLocation) {
|
||||
sayWithPersona("you just respawned from dying, and you are going to try and get your stuff back. ");
|
||||
context.setAllowDestruction = (value) => { allowDestruction = value; };
|
||||
context.getAllowDestruction = () => allowDestruction;
|
||||
|
||||
// Set goal to walk back to where Cletus died
|
||||
const goal = new GoalNear(lastDeathLocation.x, lastDeathLocation.y, lastDeathLocation.z, 2);
|
||||
bot.pathfinder.setGoal(goal);
|
||||
const originalDig = bot.dig;
|
||||
|
||||
const recoverTimeout = setTimeout(() => {
|
||||
if (bot.entity.position.distanceTo(lastDeathLocation) <= 3) {
|
||||
sayWithPersona("after dying, you actually found your stuff. ");
|
||||
isRecoveringItems = false;
|
||||
recoveryFails = 0;
|
||||
} else {
|
||||
sayWithPersona("after respawning and looking for your dropped items, you can't find the stuff. ");
|
||||
recoveryFails++;
|
||||
|
||||
if (recoveryFails >= 2) {
|
||||
sayWithPersona("after respawning multiple times in attempt to locate your dropped items from death, you give up. ");
|
||||
isRecoveringItems = false;
|
||||
lastDeathLocation = null;
|
||||
bot.pathfinder.setGoal(null); // stop movement on failure
|
||||
}
|
||||
}
|
||||
clearTimeout(recoverTimeout);
|
||||
}, 15000); // Try for 15 seconds
|
||||
bot.dig = async function (block, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSafeZones(db, async (err, zones) => {
|
||||
if (err) {
|
||||
console.warn('[SAFEZONE] Error loading zones:', err);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const defaultMove = new Movements(bot);
|
||||
bot.pathfinder.setMovements(defaultMove);
|
||||
// Check override first
|
||||
if (context.getAllowDestruction() === true) {
|
||||
try {
|
||||
const result = await originalDig.call(bot, block, ...args);
|
||||
console.log(`[DIG-OVERRIDE] ${block.name} at ${block.position}`);
|
||||
return resolve(result);
|
||||
} catch (digErr) {
|
||||
return reject(digErr);
|
||||
}
|
||||
}
|
||||
|
||||
// Passive idle wandering
|
||||
startWandering();
|
||||
// Block dig inside safe zone
|
||||
if (isInSafeZone(block.position, zones)) {
|
||||
bot.chat("You're not allowed to dig here — it's a protected zone.");
|
||||
console.log(`[BLOCKED] Attempted to dig inside safe zone: ${block.name} at ${block.position}`);
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// === Bot Chat Listener ===
|
||||
bot.on('chat', async (username, message) => {
|
||||
if (username !== BOT_NAME) {
|
||||
bot.lastChatMessage = message;
|
||||
|
||||
const messageLower = message.toLowerCase();
|
||||
const tasks = getAvailableTasks();
|
||||
|
||||
if (messageLower.includes('stop that') || messageLower.includes("don't do that")) {
|
||||
if (currentTask) {
|
||||
sayWithPersona("you have been asked to stop doing " + `${currentTask}.`);
|
||||
|
||||
// Log negative feedback
|
||||
db.get(`SELECT score FROM tasks WHERE action = ? ORDER BY timestamp DESC LIMIT 1`, [currentTask], (err, row) => {
|
||||
let score = row?.score ?? 0;
|
||||
score = Math.min(5, score + 1);
|
||||
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'interrupted', ?)`, [currentTask, score]);
|
||||
|
||||
if (score >= 5) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const script = fs.readFileSync(path.join(TASK_DIR, `${currentTask}.js`), 'utf8');
|
||||
requestTaskRewrite(currentTask, script);
|
||||
// Normal dig
|
||||
try {
|
||||
const result = await originalDig.call(bot, block, ...args);
|
||||
console.log(`[DIG] ${block.name} at ${block.position} (allowed)`);
|
||||
return resolve(result);
|
||||
} catch (digErr) {
|
||||
return reject(digErr);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Cancel current goal (pathfinder)
|
||||
bot.pathfinder.setGoal(null);
|
||||
currentTask = null;
|
||||
return;
|
||||
} else {
|
||||
sayWithPersona("you've been asked to stop doing whatever you are doing.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CHAT] ${username}: ${messageLower}`);
|
||||
console.log(`[TASK FILES LOADED]: ${Object.keys(tasks).join(', ')}`);
|
||||
|
||||
const matchedTask = Object.keys(tasks).find(taskName =>
|
||||
messageLower.includes(taskName.replace(/-/g, ' '))
|
||||
);
|
||||
|
||||
if (matchedTask) {
|
||||
console.log(`[TASK MATCHED]: ${matchedTask}`);
|
||||
sayWithPersona("you were asked to " + `${matchedTask.replace(/-/g, ' ')}.`);
|
||||
|
||||
try {
|
||||
await tasks[matchedTask](bot, db, sayWithPersona);
|
||||
db.run(`INSERT INTO tasks (action, outcome, score) VALUES (?, 'success', 0)`, [matchedTask]);
|
||||
} catch (err) {
|
||||
console.error(`Task ${matchedTask} failed:`, err.message);
|
||||
sayWithPersona("you failed at the task of " + `${matchedTask}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[NO MATCH]: Responding only.`);
|
||||
sayWithPersona(message);
|
||||
}
|
||||
}
|
||||
bot.once('spawn', async () => {
|
||||
console.log(`${config.bot.name} has spawned.`);
|
||||
const stateMachine = new StateMachine(path.join(__dirname, 'states'));
|
||||
context.setStateMachine(stateMachine);
|
||||
await stateMachine.run('Observe');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// === Lifecycle & Error Events ===
|
||||
bot.on('error', (err) => console.error('Bot error:', err));
|
||||
bot.on('kicked', (reason) => console.log('Bot was kicked:', reason));
|
||||
bot.on('end', () => console.log('Bot has disconnected.'));
|
||||
|
||||
// === Reaction to Death ===
|
||||
|
||||
let lastDeathLocation = null;
|
||||
let isRecoveringItems = false;
|
||||
let recoveryFails = 0;
|
||||
bot.on('chat', (username, message) => {
|
||||
if (username !== bot.username) {
|
||||
context.setLastChat({ username, message });
|
||||
context.getStateMachine().transition('HandleChat');
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('death', () => {
|
||||
console.log('Cletus died.');
|
||||
|
||||
let times = recoveryFails == 0 ? "." : " again.";
|
||||
|
||||
sayWithPersona("you just died" + `${times}`);
|
||||
|
||||
// Save death location
|
||||
lastDeathLocation = bot.entity.position.clone();
|
||||
isRecoveringItems = true;
|
||||
recoveryFails = 0;
|
||||
});
|
||||
|
||||
// === Reaction to Being Attacked ===
|
||||
bot.on('entityHurt', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
sayWithPersona("you got hurt.");
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: log attacker
|
||||
bot.on('entitySwingArm', (entity) => {
|
||||
if (entity.position.distanceTo(bot.entity.position) < 3) {
|
||||
const attacker = entity.username || entity.name;
|
||||
sayWithPersona(`${attacker}` + "has just hit you.");
|
||||
}
|
||||
context.getStateMachine().transition('ActOnMemory');
|
||||
});
|
||||
188
bot/bot_old.js
188
bot/bot_old.js
|
|
@ -1,188 +0,0 @@
|
|||
const mineflayer = require('mineflayer');
|
||||
const { pathfinder, Movements, goals: { GoalNear } } = require('mineflayer-pathfinder');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const axios = require('axios');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// === SQLite setup ===
|
||||
const dbPath = path.join(__dirname, '../db/memory.db');
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Failed to connect to database:', err);
|
||||
} else {
|
||||
console.log('Connected to SQLite database.');
|
||||
db.run(`CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
action TEXT,
|
||||
parameters TEXT,
|
||||
context TEXT,
|
||||
outcome TEXT,
|
||||
score INTEGER
|
||||
)`);
|
||||
}
|
||||
});
|
||||
|
||||
// === AI Integration (Ollama) ===
|
||||
const OLLAMA_URL = 'http://192.168.1.3:11434/api/generate';
|
||||
const MODEL_NAME = 'gemma3';
|
||||
|
||||
// === Create the bot ===
|
||||
const bot = mineflayer.createBot({
|
||||
host: '192.168.1.90',
|
||||
port: 25565,
|
||||
username: 'Cletus',
|
||||
version: '1.20.4',
|
||||
auth: 'offline'
|
||||
});
|
||||
|
||||
bot.loadPlugin(pathfinder);
|
||||
|
||||
bot.on('spawn', () => {
|
||||
console.log('Bot has spawned!');
|
||||
const defaultMove = new Movements(bot);
|
||||
bot.pathfinder.setMovements(defaultMove);
|
||||
|
||||
// Passive background wandering when idle
|
||||
setInterval(() => {
|
||||
if (!bot.pathfinder.isMoving() && !bot.targetDigBlock) {
|
||||
const pos = bot.entity.position.offset(
|
||||
Math.floor(Math.random() * 10 - 5),
|
||||
0,
|
||||
Math.floor(Math.random() * 10 - 5)
|
||||
);
|
||||
bot.chat("Ugh, wandering again. This server is so boring...");
|
||||
bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1));
|
||||
}
|
||||
}, 60000); // every 60 seconds
|
||||
});
|
||||
|
||||
async function followPlayer(username) {
|
||||
const target = bot.players[username]?.entity;
|
||||
if (target) {
|
||||
bot.chat(`Ugh, fine. Following ${username}.`);
|
||||
bot.pathfinder.setGoal(new GoalFollow(target, 1), true);
|
||||
db.run(`INSERT INTO tasks (action, parameters, context, outcome, score) VALUES (?, ?, ?, ?, ?)`,
|
||||
['follow', JSON.stringify({ target: username }), 'Follow player', 'started', 0]);
|
||||
} else {
|
||||
bot.chat("Seriously? I can't even see you.");
|
||||
db.run(`INSERT INTO tasks (action, parameters, context, outcome, score) VALUES (?, ?, ?, ?, ?)`,
|
||||
['follow', JSON.stringify({ target: username }), 'Follow player', 'target not found', -1]);
|
||||
}
|
||||
}
|
||||
|
||||
async function exploreArea() {
|
||||
const pos = bot.entity.position.offset(
|
||||
Math.floor(Math.random() * 20 - 10),
|
||||
0,
|
||||
Math.floor(Math.random() * 20 - 10)
|
||||
);
|
||||
bot.chat("Exploring... because why not.");
|
||||
bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1));
|
||||
db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`,
|
||||
['explore', `Random target: ${pos}`, 'started', 0]);
|
||||
}
|
||||
|
||||
async function digNearestWood() {
|
||||
const logBlock = bot.findBlock({ matching: block => block.name.includes("log"), maxDistance: 16 });
|
||||
if (!logBlock) {
|
||||
bot.chat("Wow, no trees? Shocking.");
|
||||
db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`,
|
||||
['chop_tree', 'No logs nearby', 'failed', -1]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
bot.chat("Here we go again, chopping a tree.");
|
||||
await bot.dig(logBlock);
|
||||
db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`,
|
||||
['chop_tree', `Block: ${logBlock.name}`, 'success', 1]);
|
||||
} catch (e) {
|
||||
bot.chat("Can't even chop right now. Figures.");
|
||||
db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`,
|
||||
['chop_tree', 'Dig error', 'failed', -1]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAICommand(text, username) {
|
||||
const lowered = text.toLowerCase();
|
||||
|
||||
if (lowered.includes("follow")) {
|
||||
return followPlayer(username);
|
||||
}
|
||||
if (lowered.includes("explore")) {
|
||||
return exploreArea();
|
||||
}
|
||||
if (lowered.includes("chop") && lowered.includes("tree")) {
|
||||
return digNearestWood();
|
||||
}
|
||||
if (lowered.includes("dig") && lowered.includes("dirt")) {
|
||||
const block = bot.blockAt(bot.entity.position.offset(0, -1, 0));
|
||||
if (block && bot.canDigBlock(block)) {
|
||||
bot.chat("Fine. Digging dirt. Happy now?");
|
||||
await bot.dig(block);
|
||||
db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`,
|
||||
['dig_dirt', `Block below: ${block.name}`, 'success', 1]);
|
||||
} else {
|
||||
bot.chat("Wow, I can't even dig here.");
|
||||
db.run(`INSERT INTO tasks (action, context, outcome, score) VALUES (?, ?, ?, ?)`,
|
||||
['dig_dirt', 'nothing to dig', 'fail', -1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
bot.chat(text); // Fallback
|
||||
}
|
||||
|
||||
bot.on('chat', async (username, message) => {
|
||||
if (username === bot.username) return;
|
||||
console.log(`${username}: ${message}`);
|
||||
|
||||
const prompt = `
|
||||
You are Cletus, a sarcastic teenage Minecraft bot.
|
||||
|
||||
The player said: "${message}"
|
||||
|
||||
Determine:
|
||||
1. If it's small talk, reply sarcastically.
|
||||
2. If it's a command (dig, mine, follow, chop, build), return an actionable command.
|
||||
|
||||
Format your answer like this:
|
||||
TYPE: chat - [text to say] OR TYPE: action - [command to execute]
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await axios.post(OLLAMA_URL, {
|
||||
model: MODEL_NAME,
|
||||
prompt,
|
||||
stream: false
|
||||
});
|
||||
|
||||
const aiReply = response.data?.response?.trim();
|
||||
if (aiReply.startsWith("TYPE: chat")) {
|
||||
const msg = aiReply.split(" - ")[1];
|
||||
bot.chat(msg);
|
||||
} else if (aiReply.startsWith("TYPE: action")) {
|
||||
const cmd = aiReply.split(" - ")[1];
|
||||
await handleAICommand(cmd, username);
|
||||
} else {
|
||||
bot.chat("I'm confused. You confuse me.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('AI call failed:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('error', (err) => console.error('Bot error:', err));
|
||||
bot.on('kicked', (reason) => console.log('Bot was kicked:', reason));
|
||||
bot.on('end', () => console.log('Bot has disconnected.'));
|
||||
22
bot/config.json
Normal file
22
bot/config.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"bot": {
|
||||
"name": "Cletus",
|
||||
"host": "192.168.1.90",
|
||||
"port": 25565,
|
||||
"auth": "offline",
|
||||
"version": "1.20.4"
|
||||
},
|
||||
"ai": {
|
||||
"model": "gemma3",
|
||||
"ollamaUrl": "http://localhost:11434/api/generate",
|
||||
"persona": "a curious minecraft explorer who learns from the world around them",
|
||||
"responseLength": 30
|
||||
},
|
||||
"safeZone": {
|
||||
"xBound": 50,
|
||||
"yBound": 30,
|
||||
"zBound": 50,
|
||||
"keywords": ["home", "bed", "base", "village", "farm"]
|
||||
}
|
||||
}
|
||||
|
||||
20
bot/core/context.js
Normal file
20
bot/core/context.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// core/context.js
|
||||
let bot = null;
|
||||
let stateMachine = null;
|
||||
let lastChat = null;
|
||||
|
||||
module.exports = {
|
||||
setBot: (instance) => { bot = instance; },
|
||||
getBot: () => bot,
|
||||
|
||||
setStateMachine: (machine) => { stateMachine = machine; },
|
||||
getStateMachine: () => stateMachine,
|
||||
|
||||
setLastChat: (chat) => { lastChat = chat; },
|
||||
getLastChat: () => lastChat,
|
||||
|
||||
getContextSnapshot: () => ({
|
||||
bot,
|
||||
lastChat
|
||||
})
|
||||
};
|
||||
55
bot/core/state-machine.js
Normal file
55
bot/core/state-machine.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// core/state-machine.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class StateMachine {
|
||||
constructor(statesPath) {
|
||||
this.statesPath = statesPath;
|
||||
this.states = new Map();
|
||||
this.currentState = null;
|
||||
this.loadStates();
|
||||
}
|
||||
|
||||
loadStates() {
|
||||
const files = fs.readdirSync(this.statesPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.js')) {
|
||||
const stateName = path.basename(file, '.js');
|
||||
const stateModule = require(path.join(this.statesPath, file));
|
||||
this.states.set(stateName, stateModule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async run(initialState) {
|
||||
if (!this.states.has(initialState)) {
|
||||
throw new Error(`Initial state '${initialState}' not found.`);
|
||||
}
|
||||
this.currentState = initialState;
|
||||
await this.executeCurrent();
|
||||
}
|
||||
|
||||
async transition(nextState) {
|
||||
if (!this.states.has(nextState)) {
|
||||
console.warn(`State '${nextState}' not found. Ignoring.`);
|
||||
return;
|
||||
}
|
||||
this.currentState = nextState;
|
||||
await this.executeCurrent();
|
||||
}
|
||||
|
||||
async executeCurrent() {
|
||||
const stateFn = this.states.get(this.currentState);
|
||||
if (typeof stateFn !== 'function') {
|
||||
console.warn(`State '${this.currentState}' is not executable.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await stateFn();
|
||||
} catch (err) {
|
||||
console.error(`Error executing state '${this.currentState}':`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { StateMachine };
|
||||
35
bot/db.js
Normal file
35
bot/db.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// db.js
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
const db = new sqlite3.Database(path.join(__dirname, 'db', 'memory.db'), (err) => {
|
||||
if (err) {
|
||||
console.error('[DB] Connection error:', err.message);
|
||||
} else {
|
||||
console.log('[DB] Connected to SQLite database.');
|
||||
}
|
||||
});
|
||||
|
||||
db.serialize(() => {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS memory (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT UNIQUE,
|
||||
data TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
action TEXT,
|
||||
parameters TEXT,
|
||||
context TEXT,
|
||||
outcome TEXT,
|
||||
score INTEGER DEFAULT 0
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
module.exports = db;
|
||||
|
|
@ -1,38 +1,23 @@
|
|||
// lib/ai-helper.js
|
||||
const axios = require('axios');
|
||||
const config = require('../config.json');
|
||||
|
||||
/**
|
||||
* Generates a persona-driven response from an LLM and returns the result.
|
||||
*
|
||||
* @param {string} message - The input prompt to complete.
|
||||
* @param {object} options - Persona and LLM settings.
|
||||
* @returns {Promise<string>} - A trimmed response string.
|
||||
*/
|
||||
async function chatWithPersona(message, options) {
|
||||
const {
|
||||
botName = 'Cletus',
|
||||
persona = `a cranky teenager with a sense of dark humor and sarcasm. you have short responses and don't repeat yourself.`,
|
||||
length = 20,
|
||||
ollamaUrl = 'http://192.168.1.3:11434/api/generate',
|
||||
model = 'gemma3'
|
||||
} = options;
|
||||
async function chatWithAI(message, overrides = {}) {
|
||||
|
||||
const prompt = `You are a minecraft bot named ${botName}. ` +
|
||||
`You don't know that you're a bot though. Instead, you are a ${persona}. ` +
|
||||
`Keep your responses around ${length}. Respond to this: ${message}`;
|
||||
const settings = { ...config.ai, ...overrides };
|
||||
const prompt = `You are ${settings.botName}, ${settings.persona}. Keep your responses around ${settings.responseLength} words. Respond to: ${message}`;
|
||||
|
||||
try {
|
||||
const response = await axios.post(ollamaUrl, {
|
||||
model,
|
||||
const response = await axios.post(settings.ollamaUrl, {
|
||||
model: settings.model,
|
||||
prompt,
|
||||
stream: false
|
||||
});
|
||||
|
||||
const aiReply = response.data.response?.trim().replace(/^"|"$/g, '');
|
||||
return aiReply || "...What?";
|
||||
return response.data.response?.trim() || '...';
|
||||
} catch (err) {
|
||||
console.error("AI response failed:", err.message);
|
||||
return "I'm too moody to respond right now.";
|
||||
console.error('[AI] Request failed:', err.message);
|
||||
return 'Im confused.';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { chatWithPersona };
|
||||
module.exports = { chatWithAI };
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
// lib/log-surroundings.js
|
||||
const interestingBlocks = [
|
||||
'coal_ore', 'iron_ore', 'diamond_ore', 'gold_ore', 'emerald_ore',
|
||||
'redstone_ore', 'lapis_ore', 'copper_ore',
|
||||
'oak_log', 'birch_log', 'spruce_log',
|
||||
'crafting_table', 'furnace', 'chest', 'anvil',
|
||||
'beacon', 'bell', 'lectern', 'enchanting_table', 'portal',
|
||||
'villager', 'spawner', 'campfire'
|
||||
];
|
||||
|
||||
module.exports = function logSurroundings(bot, db, sayWithPersona) {
|
||||
try {
|
||||
const blocks = bot.findBlocks({
|
||||
matching: block => interestingBlocks.includes(block.name),
|
||||
maxDistance: 12,
|
||||
count: 10
|
||||
});
|
||||
|
||||
for (const pos of blocks) {
|
||||
const block = bot.blockAt(pos);
|
||||
if (!block || !block.name) continue;
|
||||
|
||||
const label = `found-${block.name}-${pos.x},${pos.y},${pos.z}`;
|
||||
|
||||
db.run(
|
||||
`INSERT OR IGNORE INTO memory (label, data) VALUES (?, ?)`,
|
||||
[label, JSON.stringify(pos)],
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error("DB insert failed in logSurroundings:", err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("logSurroundings failed:", err);
|
||||
sayWithPersona("your brain short-circuited while trying to remember stuff.");
|
||||
}
|
||||
};
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// lib/memory.js
|
||||
|
||||
module.exports = {
|
||||
saveMemory: function (db, label, data, callback = () => {}) {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO memory (label, data) VALUES (?, ?)`,
|
||||
[label, JSON.stringify(data)],
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
getMemory: function (db, label, callback) {
|
||||
db.get(
|
||||
`SELECT data FROM memory WHERE label = ?`,
|
||||
[label],
|
||||
(err, row) => {
|
||||
if (err) return callback(err, null);
|
||||
if (!row) return callback(null, null);
|
||||
try {
|
||||
callback(null, JSON.parse(row.data));
|
||||
} catch (e) {
|
||||
callback(e, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
forgetMemory: function (db, label, callback = () => {}) {
|
||||
db.run(
|
||||
`DELETE FROM memory WHERE label = ?`,
|
||||
[label],
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
listMemory: function (db, callback) {
|
||||
db.all(`SELECT label, data FROM memory`, [], (err, rows) => {
|
||||
if (err) return callback(err, null);
|
||||
const parsed = rows.map(row => ({
|
||||
label: row.label,
|
||||
data: (() => {
|
||||
try {
|
||||
return JSON.parse(row.data);
|
||||
} catch {
|
||||
return row.data;
|
||||
}
|
||||
})()
|
||||
}));
|
||||
callback(null, parsed);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
0
bot/lib/utils.js
Normal file
0
bot/lib/utils.js
Normal file
35
bot/memory/chat.js
Normal file
35
bot/memory/chat.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// memory/chat.js
|
||||
const { saveMemory, getMemory, listMemory } = require('./index');
|
||||
|
||||
function logChatMessage(db, username, message) {
|
||||
const label = `chat:${username.toLowerCase()}`;
|
||||
|
||||
getMemory(db, label, (err, existing) => {
|
||||
const history = Array.isArray(existing) ? existing : [];
|
||||
history.push({ message, timestamp: Date.now() });
|
||||
|
||||
// Keep the latest 20 messages
|
||||
const trimmed = history.slice(-20);
|
||||
saveMemory(db, label, trimmed);
|
||||
});
|
||||
}
|
||||
|
||||
function getLastMessageFrom(db, username, callback) {
|
||||
const label = `chat:${username.toLowerCase()}`;
|
||||
getMemory(db, label, (err, history) => {
|
||||
if (err || !history || history.length === 0) return callback(null, null);
|
||||
callback(null, history[history.length - 1]);
|
||||
});
|
||||
}
|
||||
|
||||
function listKnownPlayers(db, callback) {
|
||||
listMemory(db, (err, entries) => {
|
||||
if (err) return callback(err, []);
|
||||
const players = entries
|
||||
.filter(e => e.label.startsWith('chat:'))
|
||||
.map(e => e.label.replace('chat:', ''));
|
||||
callback(null, players);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { logChatMessage, getLastMessageFrom, listKnownPlayers };
|
||||
31
bot/memory/events.js
Normal file
31
bot/memory/events.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// memory/events.js
|
||||
const { saveMemory, getMemory, listMemory } = require('./index');
|
||||
|
||||
function logEvent(db, type, data) {
|
||||
const label = `event:${type}`;
|
||||
|
||||
getMemory(db, label, (err, existing) => {
|
||||
const events = Array.isArray(existing) ? existing : [];
|
||||
events.push({ ...data, timestamp: Date.now() });
|
||||
|
||||
const trimmed = events.slice(-50); // Keep last 50 events per type
|
||||
saveMemory(db, label, trimmed);
|
||||
});
|
||||
}
|
||||
|
||||
function getRecentEvents(db, type, callback) {
|
||||
const label = `event:${type}`;
|
||||
getMemory(db, label, callback);
|
||||
}
|
||||
|
||||
function listEventTypes(db, callback) {
|
||||
listMemory(db, (err, entries) => {
|
||||
if (err) return callback(err, []);
|
||||
const types = entries
|
||||
.filter(e => e.label.startsWith('event:'))
|
||||
.map(e => e.label.replace('event:', ''));
|
||||
callback(null, types);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { logEvent, getRecentEvents, listEventTypes };
|
||||
54
bot/memory/index.js
Normal file
54
bot/memory/index.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// memory/index.js
|
||||
const db = require('../db');
|
||||
|
||||
function saveMemory(db, label, data, callback = () => {}) {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO memory (label, data) VALUES (?, ?)`,
|
||||
[label, JSON.stringify(data)],
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function getMemory(db, label, callback) {
|
||||
db.get(
|
||||
`SELECT data FROM memory WHERE label = ?`,
|
||||
[label],
|
||||
(err, row) => {
|
||||
if (err) return callback(err, null);
|
||||
if (!row) return callback(null, null);
|
||||
try {
|
||||
callback(null, JSON.parse(row.data));
|
||||
} catch (e) {
|
||||
callback(e, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function forgetMemory(db, label, callback = () => {}) {
|
||||
db.run(`DELETE FROM memory WHERE label = ?`, [label], callback);
|
||||
}
|
||||
|
||||
function listMemory(db, callback) {
|
||||
db.all(`SELECT label, data FROM memory`, [], (err, rows) => {
|
||||
if (err) return callback(err, null);
|
||||
const parsed = rows.map(row => ({
|
||||
label: row.label,
|
||||
data: (() => {
|
||||
try {
|
||||
return JSON.parse(row.data);
|
||||
} catch {
|
||||
return row.data;
|
||||
}
|
||||
})()
|
||||
}));
|
||||
callback(null, parsed);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveMemory,
|
||||
getMemory,
|
||||
forgetMemory,
|
||||
listMemory
|
||||
};
|
||||
59
bot/memory/locations.js
Normal file
59
bot/memory/locations.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// memory/locations.js
|
||||
const { getMemory, saveMemory } = require('./index');
|
||||
const config = require('../config.json');
|
||||
|
||||
function setSafeZone(db, label, position, bounds = { x: config.safeZone.xBound, y: config.safeZone.yBound, z: config.safeZone.zBound }) {
|
||||
saveMemory(db, `safezone:${label}`, { center: position, bounds });
|
||||
}
|
||||
|
||||
function getSafeZones(db, callback) {
|
||||
db.all(`SELECT label, data FROM memory WHERE label LIKE 'safezone:%'`, [], (err, rows) => {
|
||||
if (err) return callback(err, []);
|
||||
const zones = rows.map(row => ({
|
||||
label: row.label,
|
||||
...JSON.parse(row.data)
|
||||
}));
|
||||
callback(null, zones);
|
||||
});
|
||||
}
|
||||
|
||||
function getHomeZone(db, callback) {
|
||||
getSafeZones(db, (err, zones) => {
|
||||
if (err) return callback(err, null);
|
||||
const home = zones.find(z => z.label === 'safezone:home');
|
||||
callback(null, home);
|
||||
});
|
||||
}
|
||||
|
||||
function isInSafeZone(pos, zones) {
|
||||
return zones.some(zone => {
|
||||
const { center, bounds } = zone;
|
||||
return (
|
||||
Math.abs(pos.x - center.x) <= bounds.x &&
|
||||
Math.abs(pos.y - center.y) <= bounds.y &&
|
||||
Math.abs(pos.z - center.z) <= bounds.z
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setSafeZone,
|
||||
getSafeZones,
|
||||
getHomeZone,
|
||||
isInSafeZone
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
|
||||
use this is areas to determine a safeZone:
|
||||
|
||||
const { getSafeZones, isInSafeZone } = require('../memory/locations');
|
||||
|
||||
getSafeZones(db, (err, zones) => {
|
||||
if (isInSafeZone(bot.entity.position, zones)) {
|
||||
bot.chat("You're inside a safe zone.");
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
30
bot/memory/signs.js
Normal file
30
bot/memory/signs.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// memory/signs.js
|
||||
const { saveMemory, getMemory, listMemory } = require('./index');
|
||||
|
||||
function logSign(db, text, position) {
|
||||
const label = `sign:${text.toLowerCase().replace(/\s+/g, '_').slice(0, 50)}`;
|
||||
|
||||
saveMemory(db, label, {
|
||||
text,
|
||||
position,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
function getAllSigns(db, callback) {
|
||||
listMemory(db, (err, entries) => {
|
||||
if (err) return callback(err, []);
|
||||
const signs = entries.filter(e => e.label.startsWith('sign:'));
|
||||
callback(null, signs);
|
||||
});
|
||||
}
|
||||
|
||||
function findSignContaining(db, keyword, callback) {
|
||||
getAllSigns(db, (err, signs) => {
|
||||
if (err) return callback(err, null);
|
||||
const match = signs.find(s => s.data.text.toLowerCase().includes(keyword.toLowerCase()));
|
||||
callback(null, match || null);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { logSign, getAllSigns, findSignContaining };
|
||||
14
bot/states/ActOnMemory.js
Normal file
14
bot/states/ActOnMemory.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// states/ActOnMemory.js
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const { getBot } = require('../core/context');
|
||||
const config = require('../config.json');
|
||||
|
||||
module.exports = async function ActOnMemory() {
|
||||
console.log('[STATE] ActOnMemory');
|
||||
|
||||
const prompt = 'You just died. Based on what you remember, what should you do next?';
|
||||
const response = await chatWithAI(prompt, config.ai);
|
||||
|
||||
const bot = getBot();
|
||||
bot.chat(response);
|
||||
};
|
||||
40
bot/states/HandleChat.js
Normal file
40
bot/states/HandleChat.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// states/HandleChat.js
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { getLastChat, getBot, getDB } = require('../core/context');
|
||||
const { logChatMessage } = require('../memory/chat');
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const config = require('../config.json');
|
||||
|
||||
module.exports = async function HandleChat() {
|
||||
const bot = getBot();
|
||||
const db = getDB();
|
||||
const { username, message } = getLastChat();
|
||||
const msg = message.toLowerCase();
|
||||
|
||||
console.log(`[STATE] HandleChat: ${message}`);
|
||||
logChatMessage(db, username, message);
|
||||
|
||||
// Load available task scripts from the task directory.
|
||||
const taskDir = path.join(__dirname, '../bot-tasks');
|
||||
const availableTasks = fs.readdirSync(taskDir)
|
||||
.filter(f => f.endsWith('.js'))
|
||||
.map(f => f.replace('.js', ''));
|
||||
|
||||
const matchedTask = availableTasks.find(task => msg.includes(task.replace(/-/g, ' ')));
|
||||
|
||||
if (matchedTask) {
|
||||
try {
|
||||
const task = require(path.join(taskDir, matchedTask));
|
||||
bot.chat(`Okay, I’ll try to ${matchedTask.replace(/-/g, ' ')}.`);
|
||||
await task();
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(`[TASK ERROR] ${matchedTask}:`, err.message);
|
||||
bot.chat(`I couldn't complete the task "${matchedTask}".`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await chatWithAI(message, config.ai);
|
||||
bot.chat(response);
|
||||
};
|
||||
73
bot/states/Idle.js
Normal file
73
bot/states/Idle.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// states/Idle.js
|
||||
const { getBot } = require('../core/context');
|
||||
const { GoalNear } = require('mineflayer-pathfinder').goals;
|
||||
const { getHomeZone } = require('../memory/locations');
|
||||
const db = require('../db');
|
||||
|
||||
module.exports = async function Idle() {
|
||||
const bot = getBot();
|
||||
console.log('[STATE] Idle');
|
||||
|
||||
getHomeZone(db, async (err, zone) => {
|
||||
const fallbackCenter = { x: 100, y: 64, z: 100 };
|
||||
const fallbackBounds = { x: 20, y: 10, z: 20 };
|
||||
|
||||
const center = zone?.center || fallbackCenter;
|
||||
const bounds = zone?.bounds || fallbackBounds;
|
||||
const safeRadius = Math.min(bounds.x, bounds.z);
|
||||
|
||||
const actionRoll = Math.floor(Math.random() * 3);
|
||||
|
||||
if (actionRoll === 0) {
|
||||
const grass = bot.findBlock({
|
||||
matching: block => block.name === 'tall_grass',
|
||||
maxDistance: safeRadius
|
||||
});
|
||||
|
||||
if (grass) {
|
||||
await bot.pathfinder.setGoal(new GoalNear(grass.position.x, grass.position.y, grass.position.z, 1));
|
||||
try {
|
||||
await bot.dig(grass);
|
||||
bot.chat("Trimming grass.");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
} else if (actionRoll === 1) {
|
||||
const crops = bot.findBlocks({
|
||||
matching: block => ['wheat', 'carrots', 'potatoes'].includes(block.name),
|
||||
maxDistance: safeRadius,
|
||||
count: 5
|
||||
});
|
||||
|
||||
for (const pos of crops) {
|
||||
const crop = bot.blockAt(pos);
|
||||
if (crop.metadata === 7) {
|
||||
await bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1));
|
||||
try {
|
||||
await bot.dig(crop);
|
||||
bot.chat("Harvesting a crop.");
|
||||
// TODO: Replanting logic needs to be added here. This might be a state or a task.. not sure yet.
|
||||
} catch {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
const mob = Object.values(bot.entities).find(e =>
|
||||
e.type === 'mob' &&
|
||||
e.position.distanceTo(bot.entity.position) <= safeRadius &&
|
||||
e.username !== bot.username
|
||||
);
|
||||
|
||||
if (mob) {
|
||||
bot.chat(`Engaging ${mob.name}.`);
|
||||
bot.attack(mob);
|
||||
} else {
|
||||
const dx = Math.floor(Math.random() * safeRadius * 2 - safeRadius);
|
||||
const dz = Math.floor(Math.random() * safeRadius * 2 - safeRadius);
|
||||
const pos = bot.entity.position.offset(dx, 0, dz);
|
||||
bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
16
bot/states/Observe.js
Normal file
16
bot/states/Observe.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// states/Observe.js
|
||||
const { getBot } = require('../core/context');
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const config = require('../config.json');
|
||||
|
||||
module.exports = async function Observe() {
|
||||
const bot = getBot();
|
||||
console.log('[STATE] Observe');
|
||||
|
||||
// Example: count nearby entities. I will need to add way more to this observe file later.
|
||||
const entities = Object.values(bot.entities).filter(e => e.type === 'mob');
|
||||
const msg = `I see ${entities.length} mobs nearby. What should I do?`;
|
||||
const response = await chatWithAI(msg, config.ai);
|
||||
|
||||
bot.chat(response);
|
||||
};
|
||||
38
bot/states/ReadFromSign.js
Normal file
38
bot/states/ReadFromSign.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// states/readFromSign.js
|
||||
const { getBot } = require('../core/context');
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const { setSafeZone } = require('../memory/locations');
|
||||
const config = require('../config.json');
|
||||
const db = require('../db');
|
||||
|
||||
module.exports = async function ReadFromSign() {
|
||||
const bot = getBot();
|
||||
console.log('[STATE] ReadFromSign');
|
||||
|
||||
const signs = bot.findBlocks({
|
||||
matching: block => block.name.includes('sign'),
|
||||
maxDistance: 10,
|
||||
count: 5
|
||||
});
|
||||
|
||||
for (const pos of signs) {
|
||||
try {
|
||||
const text = await bot.readSign(pos);
|
||||
const keywords = config.safeZone.keywords;
|
||||
|
||||
const matchedKeyword = keywords.find(k => text.toLowerCase().includes(k));
|
||||
if (matchedKeyword) {
|
||||
const standardizedLabel = matchedKeyword.toLowerCase();
|
||||
setSafeZone(db, standardizedLabel, pos);
|
||||
bot.chat(`You set a safe zone for "${standardizedLabel}".`);
|
||||
}
|
||||
|
||||
const response = await chatWithAI(`You read a sign that says: "${text}". What should you do?`, config.ai);
|
||||
bot.chat(response);
|
||||
break;
|
||||
|
||||
} catch (err) {
|
||||
console.warn('[SIGN] Read failed:', err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
25
bot/states/RestructureScript.js
Normal file
25
bot/states/RestructureScript.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { chatWithAI } = require('../lib/ai-helper');
|
||||
const config = require('../config.json');
|
||||
|
||||
module.exports = async function RestructureScript() {
|
||||
console.log('[STATE] RestructureScript');
|
||||
|
||||
|
||||
// Replace with actual context data later. This is place holder for getting a task script that isn't written.
|
||||
const taskName = 'example-task';
|
||||
const scriptPath = path.join(__dirname, '..', 'bot-tasks', `${taskName}.js`);
|
||||
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
console.warn(`[AI] Cannot find script: ${taskName}.js`);
|
||||
return;
|
||||
}
|
||||
|
||||
const original = fs.readFileSync(scriptPath, 'utf8');
|
||||
const prompt = `This script failed repeatedly:\n${original}\nRewrite it to be more reliable. Only return code.`;
|
||||
const response = await chatWithAI(prompt, config.ai);
|
||||
|
||||
fs.writeFileSync(scriptPath, response);
|
||||
console.log(`[AI] Rewrote ${taskName}`);
|
||||
};
|
||||
Loading…
Reference in a new issue