'use strict'; const { CJbot } = require('../../model/minecraft'); const { sleep } = require('../../utils'); function _notify(msg) { try { const { getInstance } = require('./manager'); getInstance().notifySystemEvent(msg); } catch (e) { /* ignore */ } } /** * Fleet orchestration tools — registered once with the AiManager. * The LLM calls these to activate, move, and coordinate bots across the server. * * All bot→player and bot→bot interaction uses vanilla commands only: * /invite, /trade, /msg */ /** * Build and return the full fleet tool registry. * @param {object} config - merged ai config * @param {object} memoryDB - AIMemoryDB singleton * @returns {Array} tool definitions with { name, description, parameters, category, execute } */ function buildFleetTools(config, memoryDB) { const faceBotName = config.faceBot; const storageBotName = config.storageBot; const tools = []; // ======================================== // Bot lifecycle // ======================================== tools.push({ name: 'bot_activate', category: 'fleet', description: `Bring an offline bot online so it can do work. Use before asking a bot to do anything. The bot will auto-disconnect after being idle. Available bots: ${storageBotName} (storage/items), plus any bot in the fleet.`, parameters: [ { name: 'botName', type: 'string', required: true, description: 'Name of the bot to bring online' } ], execute: async (params) => { const bot = CJbot.bots[params.botName]; if (!bot) return `Unknown bot: ${params.botName}`; if (bot.isReady) return `${params.botName} is already online`; try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); bot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); _notify(`${params.botName} is now ONLINE and ready for tasks`); return `${params.botName} is now online and ready`; } catch (err) { return `Failed to bring ${params.botName} online: ${err.message}`; } } }); tools.push({ name: 'bot_deactivate', category: 'fleet', description: 'Disconnect a bot when it is no longer needed. On-demand bots auto-disconnect after being idle, but this forces it sooner.', parameters: [ { name: 'botName', type: 'string', required: true, description: 'Name of the bot to disconnect' } ], execute: async (params) => { const bot = CJbot.bots[params.botName]; if (!bot) return `Unknown bot: ${params.botName}`; if (!bot.isReady) return `${params.botName} is already offline`; bot.autoReConnect = false; bot.quit(true); return `${params.botName} is disconnecting`; } }); // ======================================== // Movement // ======================================== tools.push({ name: 'bot_goto_player', category: 'fleet', description: 'Send a bot to a player\'s current location. The bot will pathfind there automatically. IMPORTANT: the target bot must already be online (use bot_activate first if needed).', parameters: [ { name: 'botName', type: 'string', required: true, description: 'Which bot to move' }, { name: 'playerName', type: 'string', required: true, description: 'Which player to go to' } ], execute: async (params) => { const bot = CJbot.bots[params.botName]; if (!bot || !bot.isReady) return `${params.botName} is not online — activate it first`; const player = bot.bot.players[params.playerName]; if (!player || !player.entity) return `Cannot find player ${params.playerName} — they may be too far or offline`; const nav = bot.plunginsLoaded['Navigation']; if (nav && typeof nav.handleCommand === 'function') { const result = nav.handleCommand('ai', 'goto', `${player.entity.position.x} ${player.entity.position.y} ${player.entity.position.z}`, '3'); _notify(`${params.botName} is moving to ${params.playerName} at (${Math.round(player.entity.position.x)},${Math.round(player.entity.position.y)},${Math.round(player.entity.position.z)})`); return result; } // Fallback: use goTo directly try { await bot.goTo({ where: player.entity.position, range: 3 }); return `${params.botName} arrived near ${params.playerName}`; } catch (err) { return `${params.botName} failed to reach ${params.playerName}: ${err.message}`; } } }); tools.push({ name: 'bot_come_to_face', category: 'fleet', description: `Send a bot to come to you (${faceBotName}), the face bot's location. Useful for bot-to-bot trades.`, parameters: [ { name: 'botName', type: 'string', required: true, description: 'Which bot to bring here' } ], execute: async (params) => { const targetBot = CJbot.bots[params.botName]; if (!targetBot || !targetBot.isReady) return `${params.botName} is not online — activate it first`; const faceBot = CJbot.bots[faceBotName]; if (!faceBot || !faceBot.isReady || !faceBot.bot?.entity) return 'Face bot is not online'; const nav = targetBot.plunginsLoaded['Navigation']; const pos = faceBot.bot.entity.position; if (nav && typeof nav.handleCommand === 'function') { return nav.handleCommand('ai', 'goto', `${pos.x} ${pos.y} ${pos.z}`, '3'); } try { await targetBot.goTo({ where: pos, range: 3 }); return `${params.botName} arrived at ${faceBotName}'s location`; } catch (err) { return `${params.botName} failed to reach ${faceBotName}: ${err.message}`; } } }); // ======================================== // Trading // ======================================== tools.push({ name: 'bot_trade_with_player', category: 'fleet', description: 'Have a bot initiate a trade with a player. The bot sends /trade , waits for them to accept, then you can guide the trade. The bot will auto-place any pending withdrawal items in the trade window.', parameters: [ { name: 'botName', type: 'string', required: true, description: 'Which bot should trade' }, { name: 'playerName', type: 'string', required: true, description: 'Which player to trade with' } ], execute: async (params) => { const bot = CJbot.bots[params.botName]; if (!bot || !bot.isReady) return `${params.botName} is not online — activate it first`; const player = bot.bot.players[params.playerName]; if (!player) return `Player ${params.playerName} is not online or not in range`; try { // Send trade request await bot.say(`/trade ${params.playerName}`); // Wait for trade window const window = await Promise.race([ bot.once('windowOpen'), sleep(30000).then(() => null) ]); if (!window) return `Trade request to ${params.playerName} timed out (30s)`; // If bot has Storage plugin, let it handle placing withdrawn items const storage = bot.plunginsLoaded['Storage']; if (storage && typeof storage.placeWithdrawnItemsInTrade === 'function') { await storage.placeWithdrawnItemsInTrade(window, params.playerName); } // Confirm on bot side — single click, not moveSlotItem's // pickup+putdown pair (anti-cheat flags that as bad packets) try { await bot.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ } return `Trade window opened with ${params.playerName}. Bot side confirmed.`; } catch (err) { return `Trade failed: ${err.message}`; } } }); tools.push({ name: 'face_trade_accept', category: 'fleet', description: 'Accept an incoming trade request on the face bot. Use when another bot or player is trying to trade with you.', parameters: [], execute: async () => { const faceBot = CJbot.bots[faceBotName]; if (!faceBot || !faceBot.isReady) return 'Face bot is not online'; try { faceBot.bot.chat('/trade accept'); const window = await Promise.race([ faceBot.once('windowOpen'), sleep(15000).then(() => null) ]); if (!window) return 'Trade accept timed out (15s)'; return `Trade window opened on ${faceBotName}`; } catch (err) { return `Failed to accept trade: ${err.message}`; } } }); // ======================================== // Bot status // ======================================== tools.push({ name: 'bot_status', category: 'fleet', description: 'Check whether a bot is online, its position, and health.', parameters: [ { name: 'botName', type: 'string', required: true, description: 'Which bot to check' } ], execute: async (params) => { const bot = CJbot.bots[params.botName]; if (!bot) return `Unknown bot: ${params.botName}`; if (!bot.isReady) return `${params.botName} is offline`; if (!bot.bot?.entity) return `${params.botName} is connecting (no entity yet)`; const e = bot.bot.entity; return `${params.botName}: online, health=${bot.bot.health}/20, food=${bot.bot.food}/20, pos=(${Math.round(e.position.x)},${Math.round(e.position.y)},${Math.round(e.position.z)})`; } }); tools.push({ name: 'bot_list_all', category: 'fleet', description: 'List all fleet bots and whether they are online or offline.', parameters: [], execute: async () => { const lines = []; for (const [name, bot] of Object.entries(CJbot.bots)) { if (bot.isReady && bot.bot?.entity) { const e = bot.bot.entity; lines.push(`${name}: ONLINE (${Math.round(e.position.x)},${Math.round(e.position.y)},${Math.round(e.position.z)})`); } else { lines.push(`${name}: offline`); } } return lines.join('\n'); } }); // ======================================== // Storage — read-only (no bot needed) // ======================================== tools.push({ name: 'storage_find', category: 'storage', description: 'Search the storage database for an item. Use when someone asks "how much X do we have" or "do you have any Y". Returns item names and counts. No bot needs to be online.', parameters: [ { name: 'itemName', type: 'string', required: true, description: 'Item name or partial name to search for' } ], execute: async (params) => { const Database = require('../storage/database'); const items = await Database.searchItems(params.itemName); if (!items || items.length === 0) return `Storage has no '${params.itemName}'`; return items.slice(0, 8).map(i => `${i.item_name}: ${i.total_count}`).join(', '); } }); tools.push({ name: 'storage_list', category: 'storage', description: 'List the most stocked items in storage. Use to see what is available.', parameters: [ { name: 'limit', type: 'number', required: false, description: 'Max results (default 10)' } ], execute: async (params) => { const Database = require('../storage/database'); const items = await Database.searchItems(null); const limit = params.limit || 10; return (items || []).slice(0, limit).map(i => `${i.item_name}: ${i.total_count}`).join(', '); } }); // ======================================== // Storage — actions (requires storage bot) // ======================================== tools.push({ name: 'storage_withdraw', category: 'storage', description: `Withdraw items from storage and deliver them to a player. This will: activate ${storageBotName} if offline, pull items from shulkers, move to the player, and open a trade. The whole process takes 30-90 seconds. Use this when someone asks you to get them items. After delivery, ${storageBotName} will auto-disconnect.`, parameters: [ { name: 'itemName', type: 'string', required: true, description: 'Item to withdraw (e.g. diamond, golden_carrot, iron_ingot)' }, { name: 'count', type: 'number', required: true, description: 'How many to withdraw (e.g. 64)' }, { name: 'playerName', type: 'string', required: true, description: 'Player to deliver to' } ], execute: async (params) => { const storageBot = CJbot.bots[storageBotName]; if (!storageBot) return `Storage bot '${storageBotName}' is not configured`; // Step 1: ensure storage bot is online if (!storageBot.isReady) { try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); storageBot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); } catch (err) { return `Failed to bring ${storageBotName} online: ${err.message}`; } } // Step 2: execute withdraw via storage plugin's handleCommand const storage = storageBot.plunginsLoaded['Storage']; if (!storage || typeof storage.handleCommand !== 'function') { return `Storage plugin is not loaded on ${storageBotName}`; } // handleWithdrawRequest does the full flow: withdraw → trade with player // But we need to call it properly. It expects playerName to be the recipient. try { await storageBot.interruptTask('ai'); await storage.handleWithdrawRequest(params.playerName, params.itemName, params.count); return `Withdrawing ${params.count} ${params.itemName} for ${params.playerName}. ${storageBotName} will trade with them shortly.`; } catch (err) { return `Storage withdraw failed: ${err.message}`; } } }); tools.push({ name: 'storage_scan', category: 'storage', description: `Bring ${storageBotName} online, scan the storage area to update the item database, then disconnect. Use when inventory might be stale. Takes ~30 seconds.`, parameters: [], execute: async () => { const storageBot = CJbot.bots[storageBotName]; if (!storageBot) return `Storage bot '${storageBotName}' is not configured`; if (!storageBot.isReady) { try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); storageBot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); } catch (err) { return `Failed to bring ${storageBotName} online: ${err.message}`; } } const storage = storageBot.plunginsLoaded['Storage']; if (!storage) return 'Storage plugin not loaded'; try { const result = await storage.handleCommand('ai', 'scan'); // Auto-deactivate after scan since nothing else is queued return result; } catch (err) { return `Scan failed: ${err.message}`; } } }); tools.push({ name: 'storage_organize', category: 'storage', description: `Bring ${storageBotName} online and sort everything into place: unpack mixed shulkers, file loose items into the right shulkers, and consolidate partial ones. Use when someone asks to "put items away", "sort the storage", or after a big deposit. Can take several minutes.`, parameters: [], execute: async () => { const storageBot = CJbot.bots[storageBotName]; if (!storageBot) return `Storage bot '${storageBotName}' is not configured`; if (!storageBot.isReady) { try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); storageBot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); } catch (err) { return `Failed to bring ${storageBotName} online: ${err.message}`; } } const storage = storageBot.plunginsLoaded['Storage']; if (!storage) return 'Storage plugin not loaded'; // Long operation — run in the background so the face bot keeps // chatting; report the outcome via a system event when done const notify = (text) => { try { const { getInstance } = require('./manager'); if (getInstance().isActive) getInstance().notifySystemEvent(text); } catch (e) { /* ignore */ } }; storage.handleCommand('ai', 'organize') .then(result => notify(`${storageBotName} finished organizing storage: ${result}`)) .catch(err => notify(`${storageBotName} organize failed: ${err.message}`)); return `${storageBotName} started organizing storage. It runs in the background and takes a few minutes; you'll get a system message when it finishes.`; } }); tools.push({ name: 'storage_status', category: 'storage', description: 'Get storage totals: item count, shulker count, chest count. No bot needs to be online.', parameters: [], execute: async () => { const Database = require('../storage/database'); const stats = await Database.getStats(); return `Storage: ${stats.totalItems} items in ${stats.totalShulkers} shulkers (${stats.totalChests} chests)`; } }); // ======================================== // Trade between bots // ======================================== tools.push({ name: 'bot_trade_between', category: 'fleet', description: 'Orchestrate a trade between two bots. Both bots must be online. Bot A initiates /trade with Bot B, both accept, and items can transfer. Use for restocking — e.g. ez trades a shulker of shells to Art.', parameters: [ { name: 'fromBot', type: 'string', required: true, description: 'Bot that has the items (initiates trade)' }, { name: 'toBot', type: 'string', required: true, description: 'Bot receiving the items' }, { name: 'itemName', type: 'string', required: false, description: 'Specific item to move (optional, leave blank to move whatever is in pending withdrawals)' }, { name: 'count', type: 'number', required: false, description: 'Amount to move to the other bot (optional)' } ], execute: async (params) => { const fromWrapped = CJbot.bots[params.fromBot]; const toWrapped = CJbot.bots[params.toBot]; if (!fromWrapped || !fromWrapped.isReady) return `${params.fromBot} is not online`; if (!toWrapped || !toWrapped.isReady) return `${params.toBot} is not online`; try { // fromBot sends trade request await fromWrapped.say(`/trade ${params.toBot}`); // Wait for trade window to open on fromBot const window = await Promise.race([ fromWrapped.once('windowOpen'), sleep(30000).then(() => null) ]); if (!window) return `Trade between ${params.fromBot} and ${params.toBot} timed out`; // If fromBot is the storage bot with pending withdrawals, place those items const storage = fromWrapped.plunginsLoaded['Storage']; if (storage && typeof storage.placeWithdrawnItemsInTrade === 'function') { await storage.placeWithdrawnItemsInTrade(window, params.toBot); } // fromBot confirms — single click (see anti-cheat note above) try { await fromWrapped.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ } await sleep(1000); // Wait for window to close (trade complete or timeout) await Promise.race([ fromWrapped.once('windowClose'), sleep(60000) ]); // Close window if still open try { fromWrapped.bot.closeWindow(window); } catch (e) { /* ignore */ } return `Trade from ${params.fromBot} to ${params.toBot} completed`; } catch (err) { return `Bot-to-bot trade failed: ${err.message}`; } } }); // ======================================== // FarmSupply — deposit filled shulker boxes to storage // ======================================== tools.push({ name: 'farm_empty_filled_boxes', category: 'farm', description: `Empty the "filled boxes" chest on a farm bot, depositing all filled shulker boxes into storage via ${storageBotName}. The farm bot pauses its action plugins, trades each batch of up to 12 shulkers to ${storageBotName}, then resumes. ${storageBotName} must be online or will be activated. Use when a farm bot's output chest is full and needs to be cleared. Takes 30-90 seconds.`, parameters: [ { name: 'botName', type: 'string', required: false, description: `Which bot has the FarmSupply plugin (default: ${faceBotName}). Must have FarmSupply loaded.` } ], execute: async (params) => { const farmBotName = params.botName || faceBotName; const farmBot = CJbot.bots[farmBotName]; if (!farmBot) return `Unknown bot: ${farmBotName}`; if (!farmBot.isReady) return `${farmBotName} is not online — activate it first`; const fs = farmBot.plunginsLoaded['FarmSupply']; if (!fs) return `FarmSupply plugin is not loaded on ${farmBotName}`; if (fs._resupplying) return `${farmBotName} is already running a resupply, wait for it to finish`; // Ensure storage bot is online const storageBot = CJbot.bots[storageBotName]; if (!storageBot) return `Storage bot '${storageBotName}' is not configured`; if (!storageBot.isReady) { try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); storageBot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); _notify(`${storageBotName} activated for farm deposit`); } catch (err) { return `Failed to bring ${storageBotName} online: ${err.message}`; } } // Pause farm plugins, empty boxes, resume try { const paused = fs.pauseFarmPlugins(); _notify(`${farmBotName} paused farm plugins, emptying filled boxes to storage`); await fs.emptyFilledBoxes(); await fs.resumeFarmPlugins(paused); _notify(`${farmBotName} finished emptying filled boxes, farm plugins resumed`); return `${farmBotName} emptied all filled shulker boxes to storage. Farm plugins resumed.`; } catch (err) { return `Failed to empty filled boxes on ${farmBotName}: ${err.message}`; } } }); tools.push({ name: 'farm_fill_empty_shulkers', category: 'farm', description: `Refill the "empty shulkers" chest on a farm bot. Withdraws shulker_shells and chests from ${storageBotName} if needed, crafts shulker boxes, and deposits them. ${storageBotName} must be online or will be activated. Use when the farm bot is out of empty shulker boxes or when someone asks to "refill the empty shulkers at the farm". Takes 30-90 seconds.`, parameters: [ { name: 'botName', type: 'string', required: false, description: `Which bot has the FarmSupply plugin (default: ${faceBotName}). Must have FarmSupply loaded.` } ], execute: async (params) => { const farmBotName = params.botName || faceBotName; const farmBot = CJbot.bots[farmBotName]; if (!farmBot) return `Unknown bot: ${farmBotName}`; if (!farmBot.isReady) return `${farmBotName} is not online — activate it first`; const fs = farmBot.plunginsLoaded['FarmSupply']; if (!fs) return `FarmSupply plugin is not loaded on ${farmBotName}`; if (fs._resupplying) return `${farmBotName} is already running a resupply, wait for it to finish`; // Ensure storage bot is online const storageBot = CJbot.bots[storageBotName]; if (!storageBot) return `Storage bot '${storageBotName}' is not configured`; if (!storageBot.isReady) { try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); storageBot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); _notify(`${storageBotName} activated for empty shulker refill`); } catch (err) { return `Failed to bring ${storageBotName} online: ${err.message}`; } } try { _notify(`${farmBotName} refilling empty shulkers chest`); await fs.fillEmptyShulkers(); _notify(`${farmBotName} empty shulkers chest refilled`); return `${farmBotName} empty shulkers chest refilled with freshly crafted shulker boxes.`; } catch (err) { return `Failed to fill empty shulkers on ${farmBotName}: ${err.message}`; } } }); tools.push({ name: 'farm_resupply', category: 'farm', description: `Run the full resupply cycle on a farm bot: pause farm plugins, empty "filled boxes" chest to ${storageBotName}, refill "empty shulkers" chest (crafting shulker boxes if needed), resume farm plugins. ${storageBotName} will be activated if offline. Takes 1-3 minutes. Use when a farm needs complete resupply or when someone asks to "restock the farm".`, parameters: [ { name: 'botName', type: 'string', required: false, description: `Which bot has the FarmSupply plugin (default: ${faceBotName})` } ], execute: async (params) => { const farmBotName = params.botName || faceBotName; const farmBot = CJbot.bots[farmBotName]; if (!farmBot) return `Unknown bot: ${farmBotName}`; if (!farmBot.isReady) return `${farmBotName} is not online — activate it first`; const fs = farmBot.plunginsLoaded['FarmSupply']; if (!fs) return `FarmSupply plugin is not loaded on ${farmBotName}`; if (fs._resupplying) return `${farmBotName} is already running a resupply, wait for it to finish`; // Ensure storage bot is online const storageBot = CJbot.bots[storageBotName]; if (!storageBot) return `Storage bot '${storageBotName}' is not configured`; if (!storageBot.isReady) { try { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Connection timed out after 45s')), 45000); storageBot.ensureConnected(() => { clearTimeout(timeout); resolve(); }).catch(err => { clearTimeout(timeout); reject(err); }); }); _notify(`${storageBotName} activated for farm resupply`); } catch (err) { return `Failed to bring ${storageBotName} online: ${err.message}`; } } // Run full resupply try { _notify(`${farmBotName} starting full farm resupply (empty + refill)`); await fs.resupply(); _notify(`${farmBotName} resupply complete, farm resumed`); return `${farmBotName} resupply complete: filled boxes emptied to storage, empty shulkers chest refilled, farm plugins resumed.`; } catch (err) { return `Resupply on ${farmBotName} failed: ${err.message}`; } } }); // ======================================== // Settings tools // ======================================== tools.push( { name: 'list_settings', category: 'settings', description: 'List all application settings grouped by category (ai, storage, farm). Use to see what is configurable and their current values.', parameters: [ { name: 'category', type: 'string', required: false, description: 'Filter by category: ai, storage, or farm' } ], execute: async (params) => { const settings = require('../settings/manager'); const all = params.category ? settings.getAllByCategory(params.category) : settings.getAll(); const registry = settings.getRegistry(); const lines = []; for (const r of registry) { if (params.category && r.category !== params.category) continue; lines.push(`${r.key}: ${JSON.stringify(all[r.key])} [${r.category}] ${r.description}`); } return lines.join('\n') || 'No settings found'; } }, { name: 'get_setting', category: 'settings', description: 'Get the current value of a specific setting. Use before changing a setting to see its current state.', parameters: [ { name: 'key', type: 'string', required: true, description: 'Setting key (e.g., ai.temperature, storage.scanRadius)' } ], execute: async (params) => { const settings = require('../settings/manager'); const value = settings.get(params.key); const registry = settings.getRegistry().find(r => r.key === params.key); const desc = registry ? ` (${registry.description})` : ''; return value !== undefined ? `${params.key} = ${JSON.stringify(value)}${desc}` : `Unknown setting: ${params.key}`; } }, { name: 'set_setting', category: 'settings', description: 'Change a setting value. Use to update AI behavior, storage config, or farm supply config. Changes take effect on the next AI poll cycle or immediately for ai.* settings.', parameters: [ { name: 'key', type: 'string', required: true, description: 'Setting key to change (e.g., ai.temperature, storage.scanRadius)' }, { name: 'value', type: 'string', required: true, description: 'New value (numbers and booleans as strings are auto-converted)' } ], execute: async (params) => { const settings = require('../settings/manager'); try { const newValue = await settings.set(params.key, params.value); return `Set ${params.key} = ${JSON.stringify(newValue)}`; } catch (err) { return `Failed to set ${params.key}: ${err.message}`; } } } ); tools.push( { name: 'list_prompts', category: 'settings', description: 'List all available AI prompt names and their template previews. Use to see what personalities are available.', parameters: [], execute: async (params) => { const settings = require('../settings/manager'); const prompts = settings.get('ai.prompts') || {}; const names = Object.keys(prompts); if (names.length === 0) return 'No prompts configured.'; const currentName = settings.get('ai.promptName'); const lines = names.map(n => { const marker = n === currentName ? ' [ACTIVE]' : ''; const preview = (prompts[n] || '').substring(0, 80).replace(/\n/g, ' '); return `- ${n}${marker}: ${preview}...`; }); return lines.join('\n'); } } ); // ======================================== // Memory tools // ======================================== tools.push( { name: 'remember_player', category: 'memory', description: 'Store a fact about a player so you remember it forever (survives restarts, shared with the whole fleet). Use PROACTIVELY the moment you learn something — a player mentions their base, their project, a friend, a preference. Same key overwrites, so use it to update facts too.', parameters: [ { name: 'playerName', type: 'string', required: true, description: 'Player the fact is about (not necessarily who told you)' }, { name: 'key', type: 'string', required: true, description: 'Short snake_case key: base_location, current_project, trust, friend_of, notes' }, { name: 'value', type: 'string', required: true, description: 'The fact, one sentence' } ], execute: (p) => memoryDB.setPlayerMemory(p.playerName, p.key, p.value) .then(() => `Stored ${p.key}=${p.value} for ${p.playerName}`) }, { name: 'forget_player', category: 'memory', description: 'Delete one stored fact about a player (when it was wrong or is obsolete).', parameters: [ { name: 'playerName', type: 'string', required: true, description: 'Player name' }, { name: 'key', type: 'string', required: true, description: 'Memory key to delete' } ], execute: (p) => memoryDB.deletePlayerMemory(p.playerName, p.key) .then(() => `Forgot ${p.key} for ${p.playerName}`) }, { name: 'recall_player', category: 'memory', description: 'Retrieve all stored memories about a player.', parameters: [ { name: 'playerName', type: 'string', required: true, description: 'Player name' } ], execute: async (p) => { const m = await memoryDB.getAllPlayerMemories(p.playerName); const keys = Object.keys(m); return keys.length ? `Memories: ${JSON.stringify(m)}` : `No memories for ${p.playerName}`; } }, { name: 'list_known_players', category: 'memory', description: 'List all players you have stored memories about.', parameters: [], execute: async () => { const players = await memoryDB.getAllKnownPlayers(); return players.length ? `Known players: ${players.join(', ')}` : 'No known players yet'; } }, { name: 'set_directive', category: 'memory', description: 'Set a persistent instruction for yourself (current_goal, mood, focus). Survives restarts.', parameters: [ { name: 'key', type: 'string', required: true, description: 'Directive key' }, { name: 'value', type: 'string', required: true, description: 'Directive value' } ], execute: (p) => memoryDB.setDirective(faceBotName, p.key, p.value) .then(() => `Directive set: ${p.key}=${p.value}`) }, { name: 'get_directive', category: 'memory', description: 'Retrieve a specific directive you set.', parameters: [ { name: 'key', type: 'string', required: true, description: 'Directive key' } ], execute: async (p) => { const d = await memoryDB.getDirective(faceBotName, p.key); return d !== null ? `${p.key}=${d}` : `No directive for '${p.key}'`; } }, { name: 'list_directives', category: 'memory', description: 'List all active directives.', parameters: [], execute: async () => { const all = await memoryDB.getAllDirectives(faceBotName); const keys = Object.keys(all); return keys.length ? `Directives: ${JSON.stringify(all)}` : 'No active directives'; } } ); // ======================================== // Settings (global and per-bot) // ======================================== const SettingsManager = require('../settings/manager'); tools.push({ name: 'settings_list', category: 'settings', description: 'List ALL application settings by category. Returns key, type, current value, label, and description. Use this to see what can be configured. Secret values (passwords, API keys) appear as "***".', parameters: [], execute: async () => { const all = SettingsManager.getAll(); const registry = SettingsManager.getRegistry(); const cats = {}; for (const r of registry) { const cat = r.category; if (!cats[cat]) cats[cat] = []; let displayValue = all[r.key]; if (r.type === 'secret' && typeof displayValue === 'string' && displayValue.length > 0) { displayValue = '***'; } cats[cat].push(`${r.key}=${JSON.stringify(displayValue)} (${r.type}: ${r.description})`); } const parts = []; for (const [cat, items] of Object.entries(cats)) { parts.push(`== ${cat} ==\n${items.join('\n')}`); } return parts.join('\n\n'); } }); tools.push({ name: 'settings_get', category: 'settings', description: 'Get a single setting value by key path (e.g. "ai.model", "storage.scanRadius").', parameters: [ { name: 'key', type: 'string', required: true, description: 'Setting key, e.g. ai.model, storage.scanRadius' } ], execute: async (params) => { const registry = SettingsManager.getRegistry(); const entry = registry.find(r => r.key === params.key); if (!entry) return `Unknown setting: ${params.key}`; let val = SettingsManager.get(params.key); if (entry.type === 'secret' && typeof val === 'string' && val.length > 0) { val = '***'; } return `${params.key}=${JSON.stringify(val)} (${entry.type}, ${entry.category}: ${entry.description})`; } }); tools.push({ name: 'settings_set', category: 'settings', description: 'Change a setting value. Accepts string, number, boolean, or JSON. Changes persist across restarts. Use this to reconfigure the system at runtime.', parameters: [ { name: 'key', type: 'string', required: true, description: 'Setting key to change, e.g. ai.temperature, storage.scanRadius' }, { name: 'value', type: 'string', required: true, description: 'New value (will be coerced to the setting type)' } ], execute: async (params) => { const newVal = await SettingsManager.set(params.key, params.value); return `Set ${params.key}=${JSON.stringify(newVal)}`; } }); tools.push({ name: 'settings_list_bots', category: 'settings', description: 'List all bots and their configuration. Shows autoConnect, onDemand, idleTimeout, commands, plugins, etc. Passwords are redacted.', parameters: [], execute: async () => { const names = SettingsManager.getBotNames(); if (!names.length) return 'No bots configured'; const botReg = SettingsManager.getBotSettingsRegistry(); const parts = []; for (const name of names) { const settings = SettingsManager.getBotSettings(name); if (!settings) continue; const lines = [`== ${name} ==`]; for (const br of botReg) { let val = settings[br.key]; if (br.type === 'secret' && typeof val === 'string' && val.length > 0) { val = '***'; } lines.push(` ${br.key}=${JSON.stringify(val)} (${br.type})`); } parts.push(lines.join('\n')); } return parts.join('\n\n'); } }); tools.push({ name: 'settings_get_bot', category: 'settings', description: 'Get a single bot\'s full configuration by name.', parameters: [ { name: 'botName', type: 'string', required: true, description: 'Bot name (e.g. art, ez, henry)' } ], execute: async (params) => { const settings = SettingsManager.getBotSettings(params.botName); if (!settings) return `Unknown bot: ${params.botName}`; const botReg = SettingsManager.getBotSettingsRegistry(); const lines = [`== ${params.botName} ==`]; for (const br of botReg) { let val = settings[br.key]; if (br.type === 'secret' && typeof val === 'string' && val.length > 0) { val = '***'; } lines.push(` ${br.key}=${JSON.stringify(val)} (${br.type}: ${br.label})`); } return lines.join('\n'); } }); tools.push({ name: 'settings_set_bot', category: 'settings', description: 'Change a bot configuration value. Changes apply immediately to live bots where possible. Use this to enable/disable bots, change their timeouts, modify plugins, update auth credentials, etc.', parameters: [ { name: 'botName', type: 'string', required: true, description: 'Bot name to configure (e.g. art, ez, henry)' }, { name: 'key', type: 'string', required: true, description: 'Setting key: username, password, auth, autoConnect, autoReConnect, onDemand, idleTimeout, commands, plugins, hasAi' }, { name: 'value', type: 'string', required: true, description: 'New value (coerced to setting type)' } ], execute: async (params) => { const newVal = await SettingsManager.setBotSetting(params.botName, params.key, params.value); const displayVal = (typeof newVal === 'string' && newVal.length > 0 && params.key === 'password') ? '***' : JSON.stringify(newVal); return `Set ${params.botName}.${params.key}=${displayVal}`; } }); return tools; } module.exports = { buildFleetTools };