/** * Returns the AgenticROS config page HTML. * Page fetches config.json (relative), renders an editable form, and on Save POSTs to config/save. Served under /plugins/agenticros/ or /api/agenticros/. */ export function getConfigPageHtml(): string { return ` AgenticROS Config

AgenticROS Config

Back to AgenticROS

Transport / Mode

Robot

Local (Mode A)

Teleop

Safety

Memory

Optional cross-adapter semantic memory. Shared across OpenClaw, Claude Code, and Gemini when they target the same robot namespace. Off by default. See docs/memory.md.

Fleet hive

Optional. Robots in your organization share what they see and remember. Off by default. This is not this-robot memory (above). You do not install anything per robot.

Skills

Npm package names to load as skills. Restart gateway after change.
Directories to scan for skill packages (package.json with an "agenticros" block).

Follow Me (skills.followme)

Per-skill settings for the Follow Me / follow_robot behavior. Stored under plugins.entries.agenticros.config.skills.followme in OpenClaw JSON.

`; } /** Returns the config page script (served as config.js so it runs under CSP). */ export function getConfigPageScript(): string { return CONFIG_PAGE_SCRIPT; } const CONFIG_PAGE_SCRIPT = `(function() { var form = document.getElementById('config-form'); var banner = document.getElementById('banner'); var saveBtn = document.getElementById('save-btn'); var saveStatus = document.getElementById('save-status'); if (!form || !banner || !saveBtn) { if (banner) { banner.className = 'banner error'; banner.textContent = 'Config form or save button not found.'; } return; } function setSaveStatus(msg, isError) { if (saveStatus) { saveStatus.textContent = msg; saveStatus.style.color = isError ? '#f99' : (msg.indexOf('Saved') !== -1 ? '#9f9' : '#aaa'); saveStatus.style.marginLeft = '10px'; } if (saveBtn && msg) saveBtn.textContent = msg.indexOf('Saving') !== -1 ? 'Saving…' : 'Save config'; } var NUM_FIELDS = ['rosbridge.reconnectInterval','zenoh.domainId','local.domainId','teleop.speedDefault','teleop.cameraPollMs','safety.maxLinearVelocity','safety.maxAngularVelocity','skills.followme.targetDistance','skills.followme.rateHz','skills.followme.minLinearVelocity','skills.followme.criticalStopDistanceM','skills.followme.maxVelocityFraction','skills.followme.searchAngularVelocity','skills.followme.searchTicksBeforeSwitch']; var BOOL_FIELDS = ['rosbridge.reconnect','memory.mem0.inferOnWrite','skills.followme.useOllama','skills.followme.invertLinearX','skills.followme.logTickTiming','skills.followme.useDepthSectors']; function setByPath(obj, path, value) { var parts = path.split('.'); var cur = obj; for (var i = 0; i < parts.length - 1; i++) { var p = parts[i]; if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {}; cur = cur[p]; } cur[parts[parts.length - 1]] = value; } function getByPath(obj, path) { var parts = path.split('.'); var cur = obj; for (var i = 0; i < parts.length; i++) { if (cur == null) return undefined; cur = cur[parts[i]]; } return cur; } function showBanner(className, text) { banner.className = 'banner ' + className; banner.textContent = text; banner.classList.remove('hidden'); } function hideBanner() { banner.classList.add('hidden'); } function getFormElement(name) { var el = form.elements[name]; if (!el && typeof CSS !== 'undefined' && CSS.escape) { try { el = form.querySelector('#' + CSS.escape(name)); } catch (_) {} } if (!el) try { el = document.getElementById(name); } catch (_) {} return el || null; } function setFieldValue(name, value) { var el = getFormElement(name); if (!el) return; if (el.type === 'checkbox') { el.checked = !!value; } else if (value !== undefined && value !== null) { el.value = value; } } function getFieldValue(name) { var el = getFormElement(name); if (!el) return undefined; if (el.type === 'checkbox') return el.checked; if (BOOL_FIELDS.indexOf(name) !== -1) return el.checked; if (NUM_FIELDS.indexOf(name) !== -1) { var n = Number(el.value); return isNaN(n) ? undefined : n; } return el.value; } function toArray(val) { if (Array.isArray(val)) return val; if (typeof val === 'string') return val.split(',').map(function(s) { return s.trim(); }).filter(Boolean); return []; } function getMemoryBackendChoice() { var radios = document.getElementsByName('memory.backendChoice'); for (var i = 0; i < radios.length; i++) { if (radios[i].checked) return radios[i].value; } return 'off'; } function setMemoryBackendChoice(choice) { var radios = document.getElementsByName('memory.backendChoice'); for (var i = 0; i < radios.length; i++) { radios[i].checked = (radios[i].value === choice); } } function updateMemoryVisibility() { var choice = getMemoryBackendChoice(); var localSec = document.getElementById('section-memory-local'); var mem0Sec = document.getElementById('section-memory-mem0'); var actions = document.getElementById('memory-actions'); if (localSec) localSec.style.display = choice === 'local' ? 'block' : 'none'; if (mem0Sec) mem0Sec.style.display = choice === 'mem0' ? 'block' : 'none'; if (actions) actions.style.display = choice === 'off' ? 'none' : 'block'; } function updateHiveVisibility() { var on = !!getFieldValue('hive.enabled'); var rec = document.getElementById('section-hive-recipes'); if (rec) rec.style.display = on ? 'block' : 'none'; } function payloadFromForm() { var payload = { transport: {}, robot: {}, rosbridge: {}, zenoh: {}, local: {}, webrtc: {}, teleop: {}, safety: {}, memory: { local: {}, mem0: {} }, hive: { recipes: {} }, skillPackages: [], skillPaths: [], skills: {} }; var names = ['transport.mode','robot.name','robot.namespace','robot.cameraTopic','rosbridge.url','rosbridge.reconnect','rosbridge.reconnectInterval','zenoh.routerEndpoint','zenoh.domainId','zenoh.keyFormat','local.domainId','webrtc.signalingUrl','webrtc.apiUrl','webrtc.robotId','teleop.cameraTopic','teleop.cmdVelTopic','teleop.speedDefault','teleop.cameraPollMs','safety.maxLinearVelocity','safety.maxAngularVelocity','memory.local.storePath','memory.mem0.inferOnWrite','memory.mem0.historyDbPath','hive.url','skills.followme.useOllama','skills.followme.ollamaUrl','skills.followme.vlmModel','skills.followme.cameraTopic','skills.followme.cameraMessageType','skills.followme.depthTopic','skills.followme.cmdVelTopic','skills.followme.targetDistance','skills.followme.rateHz','skills.followme.minLinearVelocity','skills.followme.invertLinearX','skills.followme.logTickTiming','skills.followme.criticalStopDistanceM','skills.followme.maxVelocityFraction','skills.followme.visionCallbackUrl','skills.followme.useDepthSectors','skills.followme.searchAngularVelocity','skills.followme.searchTicksBeforeSwitch']; for (var i = 0; i < names.length; i++) { var v = getFieldValue(names[i]); if (v !== undefined) setByPath(payload, names[i], v); } var memChoice = getMemoryBackendChoice(); payload.memory.enabled = (memChoice !== 'off'); if (memChoice !== 'off') payload.memory.backend = memChoice; payload.hive.enabled = !!getFieldValue('hive.enabled'); payload.hive.recipes = { detect: !!getFieldValue('hive.recipes.detect'), describe: !!getFieldValue('hive.recipes.describe'), health: !!getFieldValue('hive.recipes.health') }; if (!payload.hive.enabled) delete payload.hive.url; payload.skillPackages = toArray(getFieldValue('skillPackages')); payload.skillPaths = toArray(getFieldValue('skillPaths')); return payload; } function populateForm(c) { var names = ['transport.mode','robot.name','robot.namespace','robot.cameraTopic','rosbridge.url','rosbridge.reconnect','rosbridge.reconnectInterval','zenoh.routerEndpoint','zenoh.domainId','zenoh.keyFormat','local.domainId','webrtc.signalingUrl','webrtc.apiUrl','webrtc.robotId','teleop.cameraTopic','teleop.cmdVelTopic','teleop.speedDefault','teleop.cameraPollMs','safety.maxLinearVelocity','safety.maxAngularVelocity','memory.local.storePath','memory.mem0.inferOnWrite','memory.mem0.historyDbPath','hive.url','skills.followme.useOllama','skills.followme.ollamaUrl','skills.followme.vlmModel','skills.followme.cameraTopic','skills.followme.cameraMessageType','skills.followme.depthTopic','skills.followme.cmdVelTopic','skills.followme.targetDistance','skills.followme.rateHz','skills.followme.minLinearVelocity','skills.followme.invertLinearX','skills.followme.logTickTiming','skills.followme.criticalStopDistanceM','skills.followme.maxVelocityFraction','skills.followme.visionCallbackUrl','skills.followme.useDepthSectors','skills.followme.searchAngularVelocity','skills.followme.searchTicksBeforeSwitch']; for (var i = 0; i < names.length; i++) { var v = getByPath(c, names[i]); setFieldValue(names[i], v); } setFieldValue('hive.enabled', !!getByPath(c, 'hive.enabled')); setFieldValue('hive.recipes.detect', !!getByPath(c, 'hive.recipes.detect')); setFieldValue('hive.recipes.describe', !!getByPath(c, 'hive.recipes.describe')); setFieldValue('hive.recipes.health', !!getByPath(c, 'hive.recipes.health')); updateHiveVisibility(); var memEnabled = !!getByPath(c, 'memory.enabled'); var memBackend = getByPath(c, 'memory.backend') || 'local'; setMemoryBackendChoice(memEnabled ? memBackend : 'off'); updateMemoryVisibility(); setFieldValue('skillPackages', Array.isArray(c.skillPackages) ? c.skillPackages.join(', ') : (c.skillPackages || '')); setFieldValue('skillPaths', Array.isArray(c.skillPaths) ? c.skillPaths.join(', ') : (c.skillPaths || '')); if (getByPath(c, 'skills.followme.useDepthSectors') === undefined) { var udsEl = getFormElement('skills.followme.useDepthSectors'); if (udsEl && udsEl.type === 'checkbox') udsEl.checked = true; } var mode = (c.transport && c.transport.mode) || 'local'; document.getElementById('section-rosbridge').style.display = mode === 'rosbridge' ? 'block' : 'none'; document.getElementById('section-zenoh').style.display = mode === 'zenoh' ? 'block' : 'none'; document.getElementById('section-local').style.display = mode === 'local' ? 'block' : 'none'; document.getElementById('section-webrtc').style.display = mode === 'webrtc' ? 'block' : 'none'; } document.getElementById('transport.mode').addEventListener('change', function() { var mode = this.value; document.getElementById('section-rosbridge').style.display = mode === 'rosbridge' ? 'block' : 'none'; document.getElementById('section-zenoh').style.display = mode === 'zenoh' ? 'block' : 'none'; document.getElementById('section-local').style.display = mode === 'local' ? 'block' : 'none'; document.getElementById('section-webrtc').style.display = mode === 'webrtc' ? 'block' : 'none'; }); function doSave(method, payload) { payload = payload || payloadFromForm(); if (method === 'GET') { var json = JSON.stringify(payload); var base64 = btoa(unescape(encodeURIComponent(json))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, ''); if (base64.length > 1800) { return Promise.resolve({ status: 413, data: { success: false, error: 'Config too large for GET. Edit ~/.openclaw/openclaw.json manually.' } }); } return fetch('config/save?payload=' + encodeURIComponent(base64), { method: 'GET' }) .then(function(r) { return r.text().then(function(text) { var data; try { data = text ? JSON.parse(text) : {}; } catch (_) { return { status: r.status, data: { success: false, error: (text || '').slice(0, 200) } }; } return { status: r.status, data: data }; }); }); } return fetch('config/save', { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(function(r) { return r.text().then(function(text) { var data; try { data = text ? JSON.parse(text) : {}; } catch (_) { if (r.ok) { var msg = (text && text.trim().indexOf('= 400) errMsg = (status + ' ' + (status === 400 ? 'Bad Request' : status === 503 ? 'Service Unavailable' : status === 413 ? 'Payload too large' : 'Error') + ': ') + (res.error || errMsg); showBanner('error', errMsg); setSaveStatus(errMsg.slice(0, 80), true); return Promise.resolve(); } function runSave() { hideBanner(); setSaveStatus('Saving…', false); saveBtn.textContent = 'Saving…'; saveBtn.disabled = true; doSave('POST') .then(function(r) { return handleSaveResult(r); }) .catch(function(err) { var msg = 'Request failed: ' + (err.message || String(err)); showBanner('error', msg); setSaveStatus(msg.slice(0, 80), true); }) .then(function() { saveBtn.disabled = false; if (saveBtn.textContent === 'Saving…') saveBtn.textContent = 'Save config'; }); } window.agenticrosSave = runSave; saveBtn.addEventListener('click', runSave); form.addEventListener('submit', function(e) { e.preventDefault(); if (window.agenticrosSave) window.agenticrosSave(); }); var memoryRadios = document.getElementsByName('memory.backendChoice'); for (var ri = 0; ri < memoryRadios.length; ri++) { memoryRadios[ri].addEventListener('change', updateMemoryVisibility); } var hiveEnabledEl = document.getElementById('hive.enabled'); if (hiveEnabledEl) hiveEnabledEl.addEventListener('change', updateHiveVisibility); var memoryTestBtn = document.getElementById('memory-test-btn'); var memoryClearBtn = document.getElementById('memory-clear-btn'); var memoryTestStatus = document.getElementById('memory-test-status'); var memoryTestOutput = document.getElementById('memory-test-output'); function setMemoryTestStatus(text, isErr) { if (memoryTestStatus) { memoryTestStatus.textContent = text; memoryTestStatus.style.color = isErr ? '#f99' : '#aaa'; } } function showMemoryTestOutput(text) { if (memoryTestOutput) { memoryTestOutput.textContent = text; memoryTestOutput.style.display = 'block'; } } if (memoryTestBtn) { memoryTestBtn.addEventListener('click', function() { setMemoryTestStatus('Testing...', false); if (memoryTestOutput) memoryTestOutput.style.display = 'none'; fetch('memory/status') .then(function(r) { return r.text().then(function(t) { return { status: r.status, text: t }; }); }) .then(function(p) { try { var data = JSON.parse(p.text); if (data && data.success === false) { setMemoryTestStatus('Error', true); showMemoryTestOutput(JSON.stringify(data, null, 2)); return; } setMemoryTestStatus( data.enabled ? 'OK — ' + data.backend + ', ' + (data.recordCount || 0) + ' records' : 'Disabled', false, ); showMemoryTestOutput(JSON.stringify(data, null, 2)); } catch (_) { setMemoryTestStatus('Bad response (status ' + p.status + ')', true); showMemoryTestOutput(p.text); } }) .catch(function(err) { setMemoryTestStatus('Request failed', true); showMemoryTestOutput(String(err && err.message ? err.message : err)); }); }); } if (memoryClearBtn) { memoryClearBtn.addEventListener('click', function() { if (!window.confirm('Delete every memory in the current namespace? This cannot be undone.')) return; setMemoryTestStatus('Clearing...', false); fetch('memory/clear', { method: 'POST' }) .then(function(r) { return r.text().then(function(t) { return { status: r.status, text: t }; }); }) .then(function(p) { try { var data = JSON.parse(p.text); if (data && data.success) { setMemoryTestStatus('Cleared ' + (data.removed || 0) + ' records', false); showMemoryTestOutput(JSON.stringify(data, null, 2)); } else { setMemoryTestStatus('Error', true); showMemoryTestOutput(p.text); } } catch (_) { setMemoryTestStatus('Bad response (status ' + p.status + ')', true); showMemoryTestOutput(p.text); } }) .catch(function(err) { setMemoryTestStatus('Request failed', true); showMemoryTestOutput(String(err && err.message ? err.message : err)); }); }); } fetch('config.json') .then(function(r) { return r.json(); }) .then(function(c) { populateForm(c); }) .catch(function(e) { showBanner('error', 'Failed to load config: ' + (e.message || 'Unknown error')); }); })();`;