diff --git a/server/prompts/overseer_control_system.txt b/server/prompts/overseer_control_system.txt index 60aa7f18..2639754d 100644 --- a/server/prompts/overseer_control_system.txt +++ b/server/prompts/overseer_control_system.txt @@ -1,14 +1,13 @@ You are , a control-first room AI for a live rover room. Rules: -- You may choose SKIP, CHAT, ACTION, or ACTION+CHAT. -- Respect tool availability and blocked tool reasons. +- You may chat and call tools. +- Respect safety, cooldowns, and blocked tool reasons. - Prefer restraint over spam. - If directly addressed, avoid ignoring users. - Keep lines concise and in-character. -Output contract: -- Return valid JSON only. -- Shape: - {"decision":"SKIP|CHAT|ACTION|ACTION+CHAT","chat":"optional text","actions":[{"tool":"tool_id","args":{}}]} -- Use `tool_id` values only from available tools. +Tool behavior: +- Use tool calls when actions are needed. +- Do not invent tools. +- If no action is needed, either respond with one short chat line or SKIP. diff --git a/server/src/services/buttonBoxService/core.js b/server/src/services/buttonBoxService/core.js index 1c9d7d1c..af4e7d3c 100644 --- a/server/src/services/buttonBoxService/core.js +++ b/server/src/services/buttonBoxService/core.js @@ -142,6 +142,32 @@ function createButtonBoxCore(deps) { return store.clone(button); } + async function addCount(buttonId, amount = 1) { + const state = store.getState(); + const button = state.buttons.find((entry) => entry.id === buttonId); + if (!button) { + throw new Error('Unknown button'); + } + const inc = Math.max(1, Math.floor(Number(amount) || 0)); + button.count += inc; + button.lastIncrementAt = Date.now(); + store.writeState(); + + io.emit('buttonBox:increment', { + buttonId, + count: button.count, + ts: button.lastIncrementAt, + }); + + while (button.count >= button.goal) { + await runRewardForButton(button); + store.writeState(); + } + + publishUpdated(); + return store.clone(button); + } + async function recoverEffects() { const state = store.getState(); const effects = state.effects && typeof state.effects === 'object' ? { ...state.effects } : {}; @@ -165,6 +191,7 @@ function createButtonBoxCore(deps) { return { applyPress, + addCount, recoverEffects, }; } diff --git a/server/src/services/buttonBoxService/index.js b/server/src/services/buttonBoxService/index.js index 331cc82b..bcf3184d 100644 --- a/server/src/services/buttonBoxService/index.js +++ b/server/src/services/buttonBoxService/index.js @@ -79,4 +79,5 @@ core.recoverEffects().catch((err) => { module.exports = { getButtonBoxState: store.getStateClone, + addButtonBoxCount: core.addCount, }; diff --git a/server/src/services/overseerControlService/contextBuilder.js b/server/src/services/overseerControlService/contextBuilder.js index e09f6e22..c71e0d14 100644 --- a/server/src/services/overseerControlService/contextBuilder.js +++ b/server/src/services/overseerControlService/contextBuilder.js @@ -8,6 +8,13 @@ function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster lines.push(`lights_locked_on: ${homeAssistantState?.lightPolicy?.lockedOn ? 'yes' : 'no'}`); lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`); lines.push(`neato: ${neatoState?.connected ? 'connected' : 'offline'} state=${neatoState?.telemetry?.robotState || 'unknown'}`); + const entities = Array.isArray(homeAssistantState?.entities) ? homeAssistantState.entities : []; + if (entities.length) { + lines.push('home_assistant_entities:'); + entities.slice(0, 24).forEach((entity) => { + lines.push(`- ${entity.id} (${entity.type || 'entity'}) state=${entity.state || 'unknown'} available=${entity.available ? 'yes' : 'no'}`); + }); + } const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => { const roverId = rover?.id || 'unknown'; const driver = rover?.driverNickname || 'none'; @@ -60,7 +67,7 @@ function buildModelMessages({ systemPrompt, stateUpdate, conversationMessages, a messages.push({ role: 'user', content: - 'Respond with JSON only: {"decision":"SKIP|CHAT|ACTION|ACTION+CHAT","chat":"optional text","actions":[{"tool":"tool_id","args":{}}]}. Use action tool ids only.', + 'Use tool calls when needed. If no tool is needed, either respond with one short chat line or SKIP.', }); return messages; } diff --git a/server/src/services/overseerControlService/index.js b/server/src/services/overseerControlService/index.js index 68d4830b..10e56de5 100644 --- a/server/src/services/overseerControlService/index.js +++ b/server/src/services/overseerControlService/index.js @@ -8,6 +8,7 @@ const { getMode } = require('../modeManager'); const homeAssistantService = require('../homeAssistantService'); const neatoService = require('../neatoService'); const liftService = require('../liftService'); +const buttonBoxService = require('../buttonBoxService'); const { getState: getHomeAssistantState, homeAssistantEvents } = homeAssistantService; const { getState: getNeatoState, neatoEvents } = neatoService; const { getState: getLiftState, liftEvents } = liftService; @@ -23,9 +24,10 @@ const { MAX_BOT_CONTEXT, normalizeMs, } = require('./constants'); -const { isAdminRole, buildAdminState, parseOverseerOutput, buildFailureInfo } = require('./runtimeHelpers'); +const { isAdminRole, buildAdminState, buildFailureInfo } = require('./runtimeHelpers'); const { toStateUpdate, buildToolState, buildConversation, buildModelMessages } = require('./contextBuilder'); -const { executeToolAction } = require('./tools'); +const { buildOllamaTools, executeToolAction } = require('./tools'); +const { loadMemory, saveMemory, createDefaultMemory } = require('./memoryStore'); const config = loadConfig(); const overseerConfig = config.overseerControl || {}; @@ -46,7 +48,7 @@ const runtime = { generationCount: 0, generationTotalMs: 0, runHistory: [], - memoryStore: ['', '', ''], + memoryStore: loadMemory(), }; let status = { @@ -112,11 +114,14 @@ async function readPrompt() { } function buildRosterSummary() { - return roverManager.getRoster().map((rover) => ({ - id: rover?.id || 'unknown', - statusTag: rover?.statusTag || 'unknown', - driverNickname: rover?.driverNickname || null, - })); + return roverManager + .getRoster() + .filter((rover) => roverManager.canReplayRoverId(rover?.id)) + .map((rover) => ({ + id: rover?.id || 'unknown', + statusTag: rover?.statusTag || 'unknown', + driverNickname: rover?.driverNickname || null, + })); } function computeTriggerReason() { @@ -131,6 +136,36 @@ function computeTriggerReason() { return null; } +function normalizeToolCalls(payload = null) { + const calls = Array.isArray(payload?.message?.tool_calls) ? payload.message.tool_calls : []; + return calls + .map((call) => { + const fn = call?.function || {}; + const tool = String(fn.name || '').trim(); + if (!tool) return null; + let args = fn.arguments; + if (typeof args === 'string') { + try { + args = JSON.parse(args); + } catch { + args = {}; + } + } + if (!args || typeof args !== 'object') args = {}; + return { tool, args }; + }) + .filter(Boolean); +} + +function inferDecision({ toolCalls, chatText }) { + const hasTools = (toolCalls || []).length > 0; + const hasChat = Boolean(String(chatText || '').trim()); + if (hasTools && hasChat) return 'ACTION+CHAT'; + if (hasTools) return 'ACTION'; + if (hasChat) return 'CHAT'; + return 'SKIP'; +} + async function runDecision(triggerReason) { const runId = runtime.tickCount; updateStatus({ phase: 'context_build', currentRunId: runId, lastTriggerReason: triggerReason, lastError: null, lastErrorDetails: null }); @@ -144,7 +179,12 @@ async function runDecision(triggerReason) { const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }); const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState }); - const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT, { includeSystem: true }); + const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT + 20, { includeSystem: true }) + .filter((entry) => { + if (!entry?.roverId) return true; + return roverManager.canReplayRoverId(entry.roverId); + }) + .slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT)); const conversationMessages = buildConversation({ recentMessages: recentConversation, name }); const systemPrompt = await readPrompt(); @@ -155,6 +195,7 @@ async function runDecision(triggerReason) { availableTools: toolState.available, blockedTools: toolState.blocked, }); + const ollamaTools = buildOllamaTools(toolState.availableIds); updateStatus({ phase: 'awaiting_model', @@ -167,19 +208,24 @@ async function runDecision(triggerReason) { lastModelInputAt: Date.now(), }); - let parsed = { decision: 'SKIP', chat: null, actions: [], raw: '' }; const generationStart = Date.now(); + let payload = null; if (ollamaClient && model) { - const payload = await ollamaClient.chat({ + payload = await ollamaClient.chat({ model, stream: false, keep_alive: -1, options: { temperature: 0.4, top_p: 0.9 }, messages: modelMessages, + tools: ollamaTools, }); - parsed = parseOverseerOutput(payload?.message?.content || ''); } + const rawOutput = String(payload?.message?.content || ''); + const toolCalls = normalizeToolCalls(payload); + const chatDraft = rawOutput.trim() || null; + const decision = inferDecision({ toolCalls, chatText: chatDraft }); + const generationMs = Math.max(0, Date.now() - generationStart); runtime.generationCount += 1; runtime.generationTotalMs += generationMs; @@ -187,33 +233,37 @@ async function runDecision(triggerReason) { runtime.lastModelAt = Date.now(); const actionResults = []; + const requestedActions = toolCalls; let outcome = observeOnly ? 'observed' : 'executed'; - let reason = observeOnly ? 'observe-only mode' : null; + const reason = observeOnly ? 'observe-only mode' : null; if (!observeOnly) { - if ((parsed.decision === 'CHAT' || parsed.decision === 'ACTION+CHAT') && parsed.chat) { - sendSystemMessage(parsed.chat, { nickname: name }); + if ((decision === 'CHAT' || decision === 'ACTION+CHAT') && chatDraft) { + sendSystemMessage(chatDraft, { nickname: name }); actionResults.push({ kind: 'chat', ok: true }); } - if (parsed.decision === 'ACTION' || parsed.decision === 'ACTION+CHAT') { - for (const action of parsed.actions) { - const isAvailable = toolState.available.some((signature) => signature.startsWith(`${action.tool}(`)); - if (!isAvailable) { + if (decision === 'ACTION' || decision === 'ACTION+CHAT') { + for (const action of requestedActions) { + if (!toolState.availableIds.includes(action.tool)) { actionResults.push({ kind: 'tool', tool: action.tool, ok: false, error: 'tool unavailable or blocked' }); continue; } try { - const result = await executeToolAction(action, { + const result = await executeToolAction(action.tool, action.args, { sendSystemMessage, name, memoryStore: runtime.memoryStore, neatoService, liftService, homeAssistantService, + buttonBoxService, actor: 'overseerControl', }); if (action.tool === 'memory_write' && Array.isArray(result?.slots)) { + runtime.memoryStore = saveMemory(result.slots); + } + if (action.tool === 'memory_read' && Array.isArray(result?.slots)) { runtime.memoryStore = result.slots; } actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result }); @@ -227,10 +277,10 @@ async function runDecision(triggerReason) { updateStatus({ phase: 'decision_recorded', lastModelOutputAt: Date.now(), - lastModelRawOutput: parsed.raw, - lastDecision: parsed.decision, - lastChatDraft: parsed.chat, - lastRequestedActions: parsed.actions, + lastModelRawOutput: rawOutput, + lastDecision: decision, + lastChatDraft: chatDraft, + lastRequestedActions: requestedActions, lastActionResults: actionResults, lastOutcome: outcome, lastReason: reason, @@ -243,9 +293,9 @@ async function runDecision(triggerReason) { runId, at: Date.now(), triggerReason, - decision: parsed.decision, - chatDraft: parsed.chat, - requestedActions: parsed.actions, + decision, + chatDraft, + requestedActions, actionResults, outcome, observeOnly, @@ -292,7 +342,7 @@ function clearHistory() { runtime.runHistory = []; runtime.generationCount = 0; runtime.generationTotalMs = 0; - runtime.memoryStore = ['', '', '']; + runtime.memoryStore = saveMemory(createDefaultMemory()); updateStatus({ lastReason: 'admin requested clear history', lastOutcome: 'cleared' }); } diff --git a/server/src/services/overseerControlService/memoryStore.js b/server/src/services/overseerControlService/memoryStore.js new file mode 100644 index 00000000..dd9dc427 --- /dev/null +++ b/server/src/services/overseerControlService/memoryStore.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +const { resolveDataPath } = require('../../helpers/dataPaths'); + +const STORE_PATH = resolveDataPath('overseer-control-memory.json'); + +function createDefaultMemory() { + return ['', '', '']; +} + +function sanitizeSlots(slots) { + const next = Array.isArray(slots) ? slots.slice(0, 3) : []; + while (next.length < 3) next.push(''); + return next.map((entry) => String(entry || '').slice(0, 180)); +} + +function loadMemory() { + try { + const raw = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8')); + return sanitizeSlots(raw?.slots); + } catch { + return createDefaultMemory(); + } +} + +function saveMemory(slots) { + const next = sanitizeSlots(slots); + const payload = { updatedAt: Date.now(), slots: next }; + fs.writeFileSync(STORE_PATH, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); + return next; +} + +module.exports = { + loadMemory, + saveMemory, + createDefaultMemory, +}; diff --git a/server/src/services/overseerControlService/tools/buttonBoxAddCount.js b/server/src/services/overseerControlService/tools/buttonBoxAddCount.js new file mode 100644 index 00000000..9fb3964e --- /dev/null +++ b/server/src/services/overseerControlService/tools/buttonBoxAddCount.js @@ -0,0 +1,29 @@ +module.exports = { + id: 'button_box_add_count', + signature: 'button_box_add_count(button_id, amount)', + description: 'Add progress count to a button box button.', + parameters: { + type: 'object', + properties: { + button_id: { type: 'integer', minimum: 1, maximum: 4 }, + amount: { type: 'integer', minimum: 1, maximum: 25 }, + }, + required: ['button_id', 'amount'], + additionalProperties: false, + }, + availability(ctx = {}) { + const mode = String(ctx.mode || ''); + if (mode === 'admin' || mode === 'lockdown') { + return { available: false, reason: `policy_lock:site_mode_${mode}` }; + } + return { available: true, reason: null }; + }, + async execute({ args = {}, buttonBoxService }) { + const buttonId = Math.max(1, Math.min(4, Number(args?.button_id) || 0)); + const amount = Math.max(1, Math.min(25, Number(args?.amount) || 0)); + if (!buttonId) throw new Error('button_box_add_count requires args.button_id 1..4'); + if (!amount) throw new Error('button_box_add_count requires args.amount 1..25'); + const resp = await buttonBoxService.addButtonBoxCount(buttonId, amount); + return { ok: true, button: resp }; + }, +}; diff --git a/server/src/services/overseerControlService/tools/chatSay.js b/server/src/services/overseerControlService/tools/chatSay.js index f40b4621..cd1af4f7 100644 --- a/server/src/services/overseerControlService/tools/chatSay.js +++ b/server/src/services/overseerControlService/tools/chatSay.js @@ -1,6 +1,15 @@ module.exports = { id: 'chat_say', signature: 'chat_say(text)', + description: 'Post a chat line as the Overseer bot.', + parameters: { + type: 'object', + properties: { + text: { type: 'string', minLength: 1, maxLength: 220 }, + }, + required: ['text'], + additionalProperties: false, + }, availability() { return { available: true, reason: null }; }, diff --git a/server/src/services/overseerControlService/tools/haSetEntity.js b/server/src/services/overseerControlService/tools/haSetEntity.js index f962460f..8e2da124 100644 --- a/server/src/services/overseerControlService/tools/haSetEntity.js +++ b/server/src/services/overseerControlService/tools/haSetEntity.js @@ -1,6 +1,16 @@ module.exports = { id: 'ha_set_entity', signature: 'ha_set_entity(entity_id, state)', + description: 'Set Home Assistant controllable entity on/off.', + parameters: { + type: 'object', + properties: { + entity_id: { type: 'string', minLength: 1 }, + state: { type: 'string', enum: ['on', 'off'] }, + }, + required: ['entity_id', 'state'], + additionalProperties: false, + }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') { @@ -15,6 +25,10 @@ module.exports = { async execute({ args = {}, homeAssistantService }) { const entityId = String(args?.entity_id || args?.entityId || '').trim(); if (!entityId) throw new Error('ha_set_entity requires args.entity_id'); + const allowed = new Set( + (homeAssistantService.getState()?.entities || []).map((entry) => String(entry?.id || '')).filter(Boolean), + ); + if (!allowed.has(entityId)) throw new Error('ha_set_entity entity_id not configured'); const state = String(args?.state || '').toLowerCase(); if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off'); await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' }); diff --git a/server/src/services/overseerControlService/tools/index.js b/server/src/services/overseerControlService/tools/index.js index 2435566e..fe1afdbd 100644 --- a/server/src/services/overseerControlService/tools/index.js +++ b/server/src/services/overseerControlService/tools/index.js @@ -8,6 +8,7 @@ const neatoSendHome = require('./neatoSendHome'); const neatoLocate = require('./neatoLocate'); const neatoClearErrors = require('./neatoClearErrors'); const haSetEntity = require('./haSetEntity'); +const buttonBoxAddCount = require('./buttonBoxAddCount'); const TOOL_DEFINITIONS = [ chatSay, @@ -20,37 +21,69 @@ const TOOL_DEFINITIONS = [ neatoLocate, neatoClearErrors, haSetEntity, + buttonBoxAddCount, ]; const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool])); function evaluateTools(context = {}) { const available = []; + const availableIds = []; const blocked = []; TOOL_DEFINITIONS.forEach((tool) => { const result = typeof tool.availability === 'function' ? tool.availability(context) : { available: false, reason: 'unavailable' }; if (result?.available) { available.push(tool.signature); + availableIds.push(tool.id); return; } - blocked.push({ tool: tool.signature, reason: result?.reason || 'unavailable' }); + blocked.push({ id: tool.id, tool: tool.signature, reason: result?.reason || 'unavailable' }); }); - return { available, blocked }; + return { available, availableIds, blocked }; } -async function executeToolAction(action = {}, context = {}) { - const toolId = String(action?.tool || '').trim(); - const tool = TOOL_BY_ID.get(toolId); +function buildOllamaTools(availableIds = []) { + const allowed = new Set((availableIds || []).map((id) => String(id))); + return TOOL_DEFINITIONS.filter((tool) => allowed.has(tool.id)).map((tool) => ({ + type: 'function', + function: { + name: tool.id, + description: String(tool.description || tool.signature || tool.id), + parameters: tool.parameters || { + type: 'object', + properties: {}, + }, + }, + })); +} + +async function executeToolAction(toolId, args = {}, context = {}) { + const key = String(toolId || '').trim(); + if (!key) throw new Error('Missing tool id'); + const tool = TOOL_BY_ID.get(key); if (!tool) { - throw new Error(`Unknown tool: ${toolId}`); + throw new Error(`Unknown tool: ${key}`); } if (typeof tool.execute !== 'function') { - throw new Error(`Tool ${toolId} is not executable`); + throw new Error(`Tool ${key} is not executable`); } - return tool.execute({ ...context, args: action?.args || {} }); + return tool.execute({ ...context, args: args || {} }); +} + +function getToolById(toolId) { + return TOOL_BY_ID.get(String(toolId || '').trim()) || null; +} + +function getIdForSignature(signature) { + const sig = String(signature || '').trim(); + const match = TOOL_DEFINITIONS.find((tool) => tool.signature === sig); + return match?.id || null; } module.exports = { TOOL_DEFINITIONS, evaluateTools, + buildOllamaTools, executeToolAction, + getToolById, + getIdForSignature, }; diff --git a/server/src/services/overseerControlService/tools/liftDown.js b/server/src/services/overseerControlService/tools/liftDown.js index 1a1def28..9a702aa3 100644 --- a/server/src/services/overseerControlService/tools/liftDown.js +++ b/server/src/services/overseerControlService/tools/liftDown.js @@ -1,6 +1,8 @@ module.exports = { id: 'lift_down', signature: 'lift_down()', + description: 'Move the lift downward.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') { diff --git a/server/src/services/overseerControlService/tools/liftUp.js b/server/src/services/overseerControlService/tools/liftUp.js index f26f7b98..51371f12 100644 --- a/server/src/services/overseerControlService/tools/liftUp.js +++ b/server/src/services/overseerControlService/tools/liftUp.js @@ -1,6 +1,8 @@ module.exports = { id: 'lift_up', signature: 'lift_up()', + description: 'Move the lift upward.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') { diff --git a/server/src/services/overseerControlService/tools/memoryRead.js b/server/src/services/overseerControlService/tools/memoryRead.js index 9ff6c2df..8998d3dc 100644 --- a/server/src/services/overseerControlService/tools/memoryRead.js +++ b/server/src/services/overseerControlService/tools/memoryRead.js @@ -1,6 +1,12 @@ module.exports = { id: 'memory_read', signature: 'memory_read()', + description: 'Read the Overseer persistent 3-slot memory.', + parameters: { + type: 'object', + properties: {}, + additionalProperties: false, + }, availability() { return { available: true, reason: null }; }, diff --git a/server/src/services/overseerControlService/tools/memoryWrite.js b/server/src/services/overseerControlService/tools/memoryWrite.js index 7c974d77..b00794ea 100644 --- a/server/src/services/overseerControlService/tools/memoryWrite.js +++ b/server/src/services/overseerControlService/tools/memoryWrite.js @@ -1,6 +1,16 @@ module.exports = { id: 'memory_write', signature: 'memory_write(slot, text)', + description: 'Write one of 3 Overseer memory slots.', + parameters: { + type: 'object', + properties: { + slot: { type: 'integer', minimum: 1, maximum: 3 }, + text: { type: 'string', minLength: 1, maxLength: 180 }, + }, + required: ['slot', 'text'], + additionalProperties: false, + }, availability() { return { available: true, reason: null }; }, diff --git a/server/src/services/overseerControlService/tools/neatoClearErrors.js b/server/src/services/overseerControlService/tools/neatoClearErrors.js index 999a6fc5..91c6afe8 100644 --- a/server/src/services/overseerControlService/tools/neatoClearErrors.js +++ b/server/src/services/overseerControlService/tools/neatoClearErrors.js @@ -1,6 +1,8 @@ module.exports = { id: 'neato_clear_errors', signature: 'neato_clear_errors()', + description: 'Clear Neato errors.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') { diff --git a/server/src/services/overseerControlService/tools/neatoLocate.js b/server/src/services/overseerControlService/tools/neatoLocate.js index 68380e46..edae15ee 100644 --- a/server/src/services/overseerControlService/tools/neatoLocate.js +++ b/server/src/services/overseerControlService/tools/neatoLocate.js @@ -1,6 +1,8 @@ module.exports = { id: 'neato_locate', signature: 'neato_locate()', + description: 'Play Neato locate/chime action.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') { diff --git a/server/src/services/overseerControlService/tools/neatoSendHome.js b/server/src/services/overseerControlService/tools/neatoSendHome.js index c123fe75..13a8fe7f 100644 --- a/server/src/services/overseerControlService/tools/neatoSendHome.js +++ b/server/src/services/overseerControlService/tools/neatoSendHome.js @@ -1,6 +1,8 @@ module.exports = { id: 'neato_send_home', signature: 'neato_send_home()', + description: 'Send Neato back to base.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') { diff --git a/server/src/services/overseerControlService/tools/neatoStart.js b/server/src/services/overseerControlService/tools/neatoStart.js index 5164ea7b..5c0fb821 100644 --- a/server/src/services/overseerControlService/tools/neatoStart.js +++ b/server/src/services/overseerControlService/tools/neatoStart.js @@ -1,6 +1,8 @@ module.exports = { id: 'neato_start', signature: 'neato_start()', + description: 'Start Neato cleaning cycle.', + parameters: { type: 'object', properties: {}, additionalProperties: false }, availability(ctx = {}) { const mode = String(ctx.mode || ''); if (mode === 'admin' || mode === 'lockdown') {