Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions agents/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,16 @@ def run_agent_loop(profile_name: str, initial_prompt: str, interval: int = 30):
send_agent_heartbeat(next_turn_in=interval, turn_in_progress=False)
continue

# Pause check — dashboard can pause heartbeat context without restarting service
try:
with urllib.request.urlopen(f"{MC_API_URL}/agent/paused", timeout=2) as _pr:
if json.loads(_pr.read().decode()).get("paused"):
print("[loop] Paused by dashboard — skipping turn", flush=True)
send_agent_heartbeat(next_turn_in=None, turn_in_progress=False)
continue
except Exception:
pass

# Gather world state
print(f"[loop] Turn {turn_count} — gathering state...", flush=True)
turn_in_progress.set()
Expand Down
111 changes: 111 additions & 0 deletions agents/bot/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@
cursor: not-allowed;
}
.bp-save-bar button:hover:not(:disabled) { background: #2ea043; }
/* Admin Controls panel */
.adm-row { display: flex; gap: 8px; align-items: center; padding: 6px 0; flex-wrap: wrap; }
.adm-row label { color: #8b949e; font-size: 0.8rem; min-width: 72px; }
.adm-row input[type=text], .adm-row input[type=number], .adm-row input[type=password] {
background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
padding: 4px 8px; border-radius: 4px; font-size: 0.82rem; }
.adm-row button {
background: #21262d; border: 1px solid #30363d; color: #c9d1d9;
padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 0.8rem; }
.adm-row button:hover { background: #30363d; }
#adm-status { font-size: 0.78rem; color: #8b949e; padding-top: 4px; }
#adm-agent-state { font-size: 0.78rem; color: #7ee787; }
</style>
</head>
<body>
Expand Down Expand Up @@ -390,6 +402,31 @@ <h1>🎮 DaemonCraft Dashboard — <span id="bot-name">Bot</span></h1>
<div class="empty">Waiting for agent turns...</div>
</div>
</div>

<!-- Admin Controls -->
<div class="panel">
<div class="panel-header" data-panel="admin">Admin Controls<button class="panel-toggle">▼</button></div>
<div class="panel-body" id="admin-panel">
<div class="adm-row">
<label>Servidor MC</label>
<input id="adm-host" type="text" style="width:140px" />
<span style="color:#8b949e">:</span>
<input id="adm-port" type="number" style="width:72px" />
<button onclick="adminSwitchServer()">Switch &amp; Restart</button>
</div>
<div class="adm-row">
<label>Token</label>
<input id="adm-token" type="password" placeholder="(vacío si no hay token)" style="width:200px" />
</div>
<div class="adm-row">
<button onclick="adminRestart()">Restart Service</button>
<button onclick="adminPause()">⏸ Pause Agent</button>
<button onclick="adminResume()">▶ Resume Agent</button>
<span id="adm-agent-state"></span>
</div>
<div id="adm-status"></div>
</div>
</div>
</div>

<script>
Expand Down Expand Up @@ -1350,6 +1387,80 @@ <h1>🎮 DaemonCraft Dashboard — <span id="bot-name">Bot</span></h1>
fetchBlueprints();

connect();

// ── Admin Controls ─────────────────────────────────────────
function adminToken() {
return document.getElementById('adm-token')?.value || '';
}

async function adminLoadConfig() {
try {
const cfg = await fetch('/config').then(r => r.json());
const hostEl = document.getElementById('adm-host');
const portEl = document.getElementById('adm-port');
if (hostEl && !hostEl.value) hostEl.value = cfg.mc_host || '';
if (portEl && !portEl.value) portEl.value = cfg.mc_port || '';
const status = document.getElementById('adm-status');
if (status) status.textContent = `Conectado a ${cfg.mc_host}:${cfg.mc_port} como ${cfg.username}`;
} catch {}
try {
const p = await fetch('/agent/paused').then(r => r.json());
const el = document.getElementById('adm-agent-state');
if (el) el.textContent = p.paused ? '⏸ Pausado' : '● Corriendo';
} catch {}
}

async function adminRestart() {
if (!confirm('¿Reiniciar el servicio DaemonCraft?')) return;
const headers = {};
const tok = adminToken();
if (tok) headers['Authorization'] = `Bearer ${tok}`;
const data = await fetch('/admin/restart', { method: 'POST', headers }).then(r => r.json()).catch(e => ({ error: e.message }));
const el = document.getElementById('adm-status');
if (el) el.textContent = data.message || data.error || 'Sent';
}

async function adminSwitchServer() {
const host = document.getElementById('adm-host')?.value.trim();
const port = parseInt(document.getElementById('adm-port')?.value);
if (!host || !port) return alert('Ingresá host y puerto');
if (!confirm(`¿Cambiar servidor a ${host}:${port} y reiniciar?`)) return;
const tok = adminToken();
const headers = { 'Content-Type': 'application/json' };
if (tok) headers['Authorization'] = `Bearer ${tok}`;
const data = await fetch('/admin/config', { method: 'POST', headers, body: JSON.stringify({ mc_host: host, mc_port: port }) })
.then(r => r.json()).catch(e => ({ error: e.message }));
const el = document.getElementById('adm-status');
if (el) el.textContent = data.message || data.error || 'Sent';
}

async function adminPause() {
const tok = adminToken();
const headers = tok ? { 'Authorization': `Bearer ${tok}` } : {};
const data = await fetch('/admin/pause', { method: 'POST', headers }).then(r => r.json()).catch(e => ({ error: e.message }));
const el = document.getElementById('adm-agent-state');
if (el) el.textContent = data.ok ? '⏸ Pausado' : (data.error || 'Error');
}

async function adminResume() {
const tok = adminToken();
const headers = tok ? { 'Authorization': `Bearer ${tok}` } : {};
const data = await fetch('/admin/resume', { method: 'POST', headers }).then(r => r.json()).catch(e => ({ error: e.message }));
const el = document.getElementById('adm-agent-state');
if (el) el.textContent = data.ok ? '● Corriendo' : (data.error || 'Error');
}

// Persist token in localStorage
document.addEventListener('DOMContentLoaded', () => {
const tokEl = document.getElementById('adm-token');
if (tokEl) {
tokEl.value = localStorage.getItem('dc_admin_token') || '';
tokEl.addEventListener('input', e => localStorage.setItem('dc_admin_token', e.target.value));
}
// Pre-load config on page load so inputs are always populated
adminLoadConfig();
});

</script>
<audio id="tts-player" preload="none" style="display:none"></audio>
</body>
Expand Down
73 changes: 70 additions & 3 deletions agents/bot/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,19 @@ function savePlan(plan) {
// Configuration
// ═══════════════════════════════════════════════════════════════════

// Override file — written by POST /admin/config, loaded at startup
const BOT_OVERRIDE_PATH = path.join(path.dirname(new URL(import.meta.url).pathname), "bot-override.json");
let fileOverrides = {};
try {
if (fs.existsSync(BOT_OVERRIDE_PATH)) {
fileOverrides = JSON.parse(fs.readFileSync(BOT_OVERRIDE_PATH, "utf8"));
}
} catch (_) {}

const config = {
mc: {
host: process.env.MC_HOST || 'localhost',
port: parseInt(process.env.MC_PORT || '25565'),
host: fileOverrides.mc_host || process.env.MC_HOST || 'localhost',
port: fileOverrides.mc_port || parseInt(process.env.MC_PORT || '25565'),
username: process.env.MC_USERNAME || 'HermesBot',
auth: process.env.MC_AUTH || 'offline',
},
Expand Down Expand Up @@ -209,6 +218,7 @@ let actionHistory = []; // { action, status, time }
const MAX_ACTION_HISTORY = 100;
let agentLog = []; // { turn, time, prompt, response, tool_calls, error }
const MAX_AGENT_LOG = 50;
let agentPaused = false;
let agentHeartbeat = { nextTurnIn: null, turnInProgress: false }; // countdown for dashboard

// ════════════════════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -3240,13 +3250,22 @@ function respond(res, status, data) {
res.end(JSON.stringify(data));
}

function requireAdminToken(req, res) {
const token = process.env.DASHBOARD_TOKEN;
if (!token) return true;
const auth = req.headers['authorization'] || '';
if (auth === `Bearer ${token}`) return true;
respond(res, 401, { ok: false, error: 'Unauthorized' });
return false;
}

const httpServer = http.createServer(async (req, res) => {
// CORS preflight
if (req.method === 'OPTIONS') {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
});
return res.end();
}
Expand All @@ -3266,6 +3285,21 @@ const httpServer = http.createServer(async (req, res) => {
});
}

if (path === '/config') {
return respond(res, 200, {
ok: true,
mc_host: config.mc.host,
mc_port: config.mc.port,
username: config.mc.username,
auth: config.mc.auth,
api_port: config.api.port,
});
}

if (path === '/agent/paused') {
return respond(res, 200, { ok: true, paused: agentPaused });
}

if (path === '/status') {
return respond(res, 200, { ok: true, data: getFullState() });
}
Expand Down Expand Up @@ -3850,6 +3884,39 @@ const httpServer = http.createServer(async (req, res) => {
// Synchronous action: POST /action/ACTION (still supported for quick stuff)
const actionMatch = path.match(/^\/action\/(\w+)$/);
if (!actionMatch) {
// ── Admin: restart, config switch, pause/resume ─────────────
if (path === '/admin/restart') {
if (!requireAdminToken(req, res)) return;
respond(res, 200, { ok: true, message: 'Restarting in 1s…' });
setTimeout(() => process.exit(0), 1000);
return;
}

if (path === '/admin/config') {
if (!requireAdminToken(req, res)) return;
const { mc_host, mc_port } = body;
if (!mc_host || !mc_port) return respond(res, 400, { ok: false, error: 'mc_host and mc_port required' });
const current = fs.existsSync(BOT_OVERRIDE_PATH)
? JSON.parse(fs.readFileSync(BOT_OVERRIDE_PATH, 'utf8'))
: {};
fs.writeFileSync(BOT_OVERRIDE_PATH, JSON.stringify({ ...current, mc_host, mc_port: parseInt(mc_port) }, null, 2));
respond(res, 200, { ok: true, mc_host, mc_port, message: 'Config saved, restarting…' });
setTimeout(() => process.exit(0), 1000);
return;
}

if (path === '/admin/pause') {
if (!requireAdminToken(req, res)) return;
agentPaused = true;
return respond(res, 200, { ok: true, paused: true });
}

if (path === '/admin/resume') {
if (!requireAdminToken(req, res)) return;
agentPaused = false;
return respond(res, 200, { ok: true, paused: false });
}

// Special: /connect
if (path === '/connect') {
await createBot();
Expand Down
Loading