@hera-al/server 1.6.29 → 1.6.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
export function configJS(){return"\n/* ---- Env var ref markers ---- */\nfunction markEnvRefs(container){\n if(!container) return;\n container.querySelectorAll('input').forEach(function(inp){\n if(/^\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}$/.test(inp.value)){\n inp.classList.add('env-ref');\n inp.title='Stored as environment variable in .env';\n } else {\n inp.classList.remove('env-ref');\n inp.title='';\n }\n });\n}\n\n/* ---- STT ---- */\nasync function loadSTT(){\n currentConfig = await fetchAPI('/config');\n const s = currentConfig.stt||{};\n document.getElementById('sttEnabled').checked = !!s.enabled;\n document.getElementById('sttProvider').value = s.provider||'openai-whisper';\n const oai = s['openai-whisper']||{};\n populateSTTModelSelect();\n var sttModels = (currentConfig&¤tConfig.models)||[];\n document.getElementById('sttModelRef').value = typeof upgradeModelRef==='function' ? upgradeModelRef(oai.modelRef||'', sttModels) : (oai.modelRef||'');\n document.getElementById('sttOAILang').value = oai.language||'';\n const loc = s['local-whisper']||{};\n document.getElementById('sttLocalBin').value = loc.binaryPath||'whisper';\n document.getElementById('sttLocalModel').value = loc.model||'base';\n updateSTTFields();\n if(!loadSTT._bound){\n document.getElementById('sttProvider').addEventListener('change', updateSTTFields);\n loadSTT._bound = true;\n }\n sectionsLoaded.stt = true;\n}\nfunction updateSTTFields(){\n const prov = document.getElementById('sttProvider').value;\n document.getElementById('sttOpenAI').style.display = prov==='openai-whisper'?'':'none';\n document.getElementById('sttLocal').style.display = prov==='local-whisper'?'':'none';\n}\n\n/* ---- Memory ---- */\nasync function loadMemory(){\n currentConfig = await fetchAPI('/config');\n const m = currentConfig.memory||{};\n document.getElementById('memEnabled').checked = !!m.enabled;\n document.getElementById('memDir').value = m.dir||'./memory';\n document.getElementById('memStrategy').value = m.recallStrategy||'builtin-only';\n // Search settings\n const s = m.search||{};\n populateMemSearchModelSelect();\n var memModels = (currentConfig&¤tConfig.models)||[];\n document.getElementById('memSearchModelRef').value = typeof upgradeModelRef==='function' ? upgradeModelRef(s.modelRef||'', memModels) : (s.modelRef||'');\n document.getElementById('memSearchEmbModel').value = s.embeddingModel||'text-embedding-3-small';\n document.getElementById('memSearchPrefixQuery').value = s.prefixQuery||'';\n document.getElementById('memSearchPrefixDocument').value = s.prefixDocument||'';\n var dimsVal = String(s.embeddingDimensions||1536);\n document.getElementById('memSearchDims').value = dimsVal;\n document.getElementById('memSearchDimsValue').textContent = dimsVal;\n document.getElementById('memSearchMaxResults').value = s.maxResults||6;\n document.getElementById('memSearchDebounce').value = s.updateDebounceMs||3000;\n document.getElementById('memSearchEmbedInterval').value = s.embedIntervalMs||300000;\n document.getElementById('memSearchMaxSnippet').value = s.maxSnippetChars||700;\n document.getElementById('memSearchMaxInjected').value = s.maxInjectedChars||4000;\n document.getElementById('memSearchRrfK').value = s.rrfK||60;\n updateMemSearchFields();\n // L0 settings\n const l0 = m.l0||{};\n document.getElementById('l0Enabled').checked = l0.enabled !== false; // default true\n document.getElementById('l0Model').value = l0.model||'';\n updateL0Hint();\n document.getElementById('l0Model').addEventListener('input', updateL0Hint);\n // Intercept toggle-off\n document.getElementById('memEnabled').addEventListener('change', function(){\n if(!this.checked){\n this.checked = true; // revert until confirmed\n document.getElementById('memDisableModal').classList.add('open');\n }\n });\n sectionsLoaded.memory = true;\n}\nfunction updateMemSearchFields(){\n var strategy = document.getElementById('memStrategy').value;\n document.getElementById('memSearchSettings').style.display = strategy==='search'?'':'none';\n}\nfunction updateL0Hint(){\n var model = (document.getElementById('l0Model').value||'').trim();\n document.getElementById('l0OpenAIHint').style.display = model ? '' : 'none';\n}\nasync function testEmbedding(){\n var btn = document.getElementById('memSearchTestBtn');\n var result = document.getElementById('memSearchTestResult');\n btn.disabled = true;\n btn.textContent = 'Testing...';\n result.style.color = 'var(--text-muted)';\n result.textContent = '';\n try {\n var body = {\n modelRef: document.getElementById('memSearchModelRef').value,\n embeddingModel: document.getElementById('memSearchEmbModel').value || 'text-embedding-3-small',\n embeddingDimensions: parseInt(document.getElementById('memSearchDims').value) || 1536\n };\n var resp = await fetch(API+'/memory-search/test-embedding', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, credentials:'include', body:JSON.stringify(body)});\n var data = await resp.json();\n if(data.ok){\n result.style.color = 'var(--success, #22c55e)';\n result.textContent = '✓ OK — model: '+data.model+', dims: '+data.dimensions+', latency: '+data.latencyMs+'ms';\n } else {\n result.style.color = 'var(--error, #ef4444)';\n result.textContent = '✗ '+data.error;\n }\n } catch(err){\n result.style.color = 'var(--error, #ef4444)';\n result.textContent = '✗ Connection error: '+err.message;\n } finally {\n btn.disabled = false;\n btn.textContent = 'Test Embedding';\n }\n}\nfunction closeMemDisableModal(confirmed){\n document.getElementById('memDisableModal').classList.remove('open');\n if(confirmed){\n document.getElementById('memEnabled').checked = false;\n saveConfig();\n }\n}\n\n/* ---- Settings ---- */\nasync function loadSettings(){\n currentConfig = await fetchAPI('/config');\n document.getElementById('settingsHost').value = currentConfig.host || '127.0.0.1';\n document.getElementById('settingsUiPort').value = (currentConfig.nostromo && currentConfig.nostromo.port) || '3001';\n document.getElementById('settingsTimezone').value = currentConfig.timezone || '';\n document.getElementById('settingsAutoRestart').checked = !!(currentConfig.nostromo && currentConfig.nostromo.autoRestart);\n document.getElementById('settingsConfigCheckInterval').value = (currentConfig.nostromo && currentConfig.nostromo.configCheckInterval) || 5;\n sectionsLoaded.settings = true;\n}\n\n/* ---- Access key display ---- */\nvar _currentKey = '';\nvar _keyRevealed = false;\n\nfunction maskKey(key){\n // Show first 4 chars, mask the rest: \"ABCD-****-****-****\"\n if(key.length<=4) return key;\n return key.substring(0,4) + key.substring(4).replace(/[A-Za-z0-9]/g, '*');\n}\n\nasync function loadCurrentKey(){\n try{\n const res = await fetch(API+'/key');\n const data = await res.json();\n _currentKey = data.key || '';\n _keyRevealed = false;\n var row = document.getElementById('currentKeyRow');\n var el = document.getElementById('currentKeyValue');\n if(_currentKey && _currentKey !== '0000'){\n el.textContent = maskKey(_currentKey);\n row.style.display = 'flex';\n } else {\n row.style.display = 'none';\n }\n document.getElementById('keyEyeOff').style.display = '';\n document.getElementById('keyEyeOn').style.display = 'none';\n }catch(e){}\n}\n\nfunction toggleKeyVisibility(){\n _keyRevealed = !_keyRevealed;\n var el = document.getElementById('currentKeyValue');\n el.textContent = _keyRevealed ? _currentKey : maskKey(_currentKey);\n document.getElementById('keyEyeOff').style.display = _keyRevealed ? 'none' : '';\n document.getElementById('keyEyeOn').style.display = _keyRevealed ? '' : 'none';\n}\n\nasync function copyKey(){\n try{\n await navigator.clipboard.writeText(_currentKey);\n document.getElementById('keyCopyIcon').style.display = 'none';\n document.getElementById('keyCheckIcon').style.display = '';\n setTimeout(function(){\n document.getElementById('keyCopyIcon').style.display = '';\n document.getElementById('keyCheckIcon').style.display = 'none';\n }, 1500);\n }catch(e){ toast('Copy failed','err'); }\n}\n\n/* ---- Regenerate key ---- */\nfunction confirmRegenKey(){\n document.getElementById('regenKeyModal').classList.add('open');\n}\nfunction closeRegenKeyModal(){\n document.getElementById('regenKeyModal').classList.remove('open');\n}\nasync function regenKey(){\n try{\n const res = await fetch(API+'/key/regenerate',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n const data = await res.json();\n if(data.key){\n showNewKey(data.key);\n _currentKey = data.key;\n _keyRevealed = false;\n var el = document.getElementById('currentKeyValue');\n el.textContent = maskKey(data.key);\n document.getElementById('currentKeyRow').style.display = 'flex';\n document.getElementById('keyEyeOff').style.display = '';\n document.getElementById('keyEyeOn').style.display = 'none';\n }\n }catch(e){ toast('Failed','err'); }\n}\n\n/* ---- Save config ---- */\nfunction gatherConfig(){\n if(!currentConfig){ toast('Config not loaded yet — cannot save','err'); return null; }\n const cfg = JSON.parse(JSON.stringify(currentConfig));\n // Verbose debug logs\n var logVerboseEl = document.getElementById('logVerbose');\n if(logVerboseEl) cfg.verboseDebugLogs = logVerboseEl.checked;\n // Channels\n const wrap = document.getElementById('channelCards');\n if(wrap){\n if(!cfg.channels) cfg.channels={};\n for(const ch of CHANNEL_LIST){\n const toggle = wrap.querySelector('[data-ch-toggle=\"'+ch+'\"]');\n if(!toggle) continue;\n if(!cfg.channels[ch]) cfg.channels[ch]={};\n cfg.channels[ch].enabled = toggle.checked;\n }\n // Collect all channel fields via data-ch-field=\"channel.account.key\" or \"channel.key\"\n wrap.querySelectorAll('[data-ch-field]').forEach(inp=>{\n const parts = inp.dataset.chField.split('.');\n const chName = parts[0];\n if(!cfg.channels[chName]) cfg.channels[chName]={};\n if(parts.length === 3){\n // channel.account.key\n const [, acct, key] = parts;\n if(!cfg.channels[chName].accounts) cfg.channels[chName].accounts={};\n if(!cfg.channels[chName].accounts[acct]) cfg.channels[chName].accounts[acct]={};\n var val;\n if(key==='allowFrom') val = inp.value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n else if(inp.type==='number') val = parseInt(inp.value)||0;\n else val = inp.value;\n cfg.channels[chName].accounts[acct][key] = val;\n } else if(parts.length === 2){\n // channel.key (e.g. responses.port)\n const key = parts[1];\n cfg.channels[chName][key] = inp.type==='number' ? (parseInt(inp.value)||0) : inp.value;\n }\n });\n }\n // Models\n if(currentConfig&¤tConfig.models) cfg.models = currentConfig.models;\n // Agent — only gather from DOM if loadAgent() has populated the fields\n if(sectionsLoaded.agent){\n cfg.agent = cfg.agent||{};\n cfg.agent.model = document.getElementById('agentModel').value;\n cfg.agent.mainFallback = document.getElementById('agentMainFallback').value;\n cfg.agent.maxTurns = parseInt(document.getElementById('agentMaxTurns').value)||10;\n cfg.agent.permissionMode = document.getElementById('agentPermMode').value;\n cfg.agent.sessionTTL = parseInt(document.getElementById('agentSessionTTL').value)||3600;\n cfg.agent.settingSources = document.getElementById('agentSettingSources').value;\n cfg.agent.builtinCoderSkill = document.getElementById('agentCoderSkill').checked;\n cfg.agent.autoRenew = parseInt(document.getElementById('agentAutoRenew').value)||0;\n cfg.agent.allowedTools = [];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){if(cb.checked) cfg.agent.allowedTools.push(cb.dataset.tool);});\n cfg.agent.disallowedTools = [];\n cfg.agent.queueMode = document.getElementById('agentQueueMode').value;\n cfg.agent.queueDebounceMs = parseInt(document.getElementById('agentDebounceMs').value)||0;\n cfg.agent.queueCap = parseInt(document.getElementById('agentQueueCap').value)||0;\n cfg.agent.queueDropPolicy = document.getElementById('agentDropPolicy').value;\n cfg.agent.inflightTyping = document.getElementById('agentInflightTyping').checked;\n cfg.agent.autoApproveTools = document.getElementById('agentAutoApprove').checked;\n // Pico Agent\n cfg.agent.picoAgent = {\n enabled: document.getElementById('picoEnabled').checked,\n modelRefs: _picoModels.map(function(m){ return m.name + ':' + m.piProvider + ':' + m.piModelId; }),\n rollingMemoryModel: document.getElementById('picoRollingModel').value\n };\n delete cfg.agent.engine; // remove legacy\n }\n // Custom SubAgents (preserved from currentConfig, mutated in-place by the UI)\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n if(!cfg.agent) cfg.agent = {};\n cfg.agent.customSubAgents = currentConfig.agent.customSubAgents;\n }\n // Plugins (preserved from currentConfig, mutated in-place by the UI)\n if(currentConfig && currentConfig.agent && currentConfig.agent.plugins){\n if(!cfg.agent) cfg.agent = {};\n cfg.agent.plugins = currentConfig.agent.plugins;\n }\n // STT — only gather from DOM if loadSTT() has populated the fields\n if(sectionsLoaded.stt){\n cfg.stt = cfg.stt||{};\n cfg.stt.enabled = document.getElementById('sttEnabled').checked;\n cfg.stt.provider = document.getElementById('sttProvider').value;\n var sttModelName = document.getElementById('sttModelRef').value;\n cfg.stt['openai-whisper'] = {\n modelRef: sttModelName,\n model: 'whisper-1',\n language: document.getElementById('sttOAILang').value\n };\n cfg.stt['local-whisper'] = {\n binaryPath: document.getElementById('sttLocalBin').value,\n model: document.getElementById('sttLocalModel').value\n };\n }\n // TTS — only gather from DOM if loadTTS() has populated the fields\n if(sectionsLoaded.tts){\n cfg.tts = cfg.tts||{};\n cfg.tts.enabled = document.getElementById('ttsEnabled').checked;\n cfg.tts.provider = document.getElementById('ttsProvider').value;\n cfg.tts.maxTextLength = parseInt(document.getElementById('ttsMaxTextLength').value)||4096;\n cfg.tts.timeoutMs = parseInt(document.getElementById('ttsTimeoutMs').value)||30000;\n cfg.tts.edge = {\n voice: document.getElementById('ttsEdgeVoice').value || 'en-US-MichelleNeural'\n };\n cfg.tts.openai = {\n modelRef: document.getElementById('ttsOAIModelRef').value,\n model: document.getElementById('ttsOAIModel').value || 'gpt-4o-mini-tts',\n voice: document.getElementById('ttsOAIVoice').value || 'alloy'\n };\n cfg.tts.elevenlabs = {\n modelRef: document.getElementById('ttsELModelRef').value,\n voiceId: document.getElementById('ttsELVoiceId').value || 'pMsXgVXv3BLzUgSXRplE',\n modelId: document.getElementById('ttsELModelId').value || 'eleven_multilingual_v2'\n };\n }\n // Memory — only gather from DOM if loadMemory() has populated the fields\n if(sectionsLoaded.memory){\n cfg.memory = cfg.memory||{};\n cfg.memory.enabled = document.getElementById('memEnabled').checked;\n cfg.memory.dir = document.getElementById('memDir').value;\n cfg.memory.recallStrategy = document.getElementById('memStrategy').value;\n cfg.memory.search = {\n enabled: cfg.memory.recallStrategy === 'search',\n modelRef: document.getElementById('memSearchModelRef').value,\n embeddingModel: document.getElementById('memSearchEmbModel').value || 'text-embedding-3-small',\n prefixQuery: document.getElementById('memSearchPrefixQuery').value || '',\n prefixDocument: document.getElementById('memSearchPrefixDocument').value || '',\n embeddingDimensions: parseInt(document.getElementById('memSearchDims').value) || 1536,\n updateDebounceMs: parseInt(document.getElementById('memSearchDebounce').value) || 3000,\n embedIntervalMs: parseInt(document.getElementById('memSearchEmbedInterval').value) || 300000,\n maxResults: parseInt(document.getElementById('memSearchMaxResults').value) || 6,\n maxSnippetChars: parseInt(document.getElementById('memSearchMaxSnippet').value) || 700,\n maxInjectedChars: parseInt(document.getElementById('memSearchMaxInjected').value) || 4000,\n rrfK: parseInt(document.getElementById('memSearchRrfK').value) || 60\n };\n cfg.memory.l0 = {\n enabled: document.getElementById('l0Enabled').checked,\n model: (document.getElementById('l0Model').value||'').trim()\n };\n }\n // Cron — only gather from DOM if loadCron() has populated the fields\n if(sectionsLoaded.cron){\n cfg.cron = cfg.cron||{};\n cfg.cron.enabled = document.getElementById('cronEnabled').checked;\n cfg.cron.isolated = document.getElementById('cronIsolated').checked;\n cfg.cron.broadcastEvents = document.getElementById('cronBroadcast').checked;\n cfg.cron.heartbeat = cfg.cron.heartbeat||{};\n cfg.cron.heartbeat.enabled = document.getElementById('hbEnabled').checked;\n cfg.cron.heartbeat.channel = document.getElementById('hbChannel').value;\n cfg.cron.heartbeat.chatId = document.getElementById('hbChatId').value;\n cfg.cron.heartbeat.every = parseInt(document.getElementById('hbEvery').value)||1800000;\n cfg.cron.heartbeat.message = document.getElementById('hbMessage').value;\n cfg.cron.heartbeat.ackMaxChars = parseInt(document.getElementById('hbAckMaxChars').value)||300;\n }\n // Settings — only gather from DOM if loadSettings() has populated the fields\n if(sectionsLoaded.settings){\n cfg.host = document.getElementById('settingsHost').value || '127.0.0.1';\n cfg.timezone = document.getElementById('settingsTimezone').value || '';\n cfg.nostromo = cfg.nostromo||{};\n cfg.nostromo.port = parseInt(document.getElementById('settingsUiPort').value)||3001;\n cfg.nostromo.autoRestart = document.getElementById('settingsAutoRestart').checked;\n cfg.nostromo.configCheckInterval = parseInt(document.getElementById('settingsConfigCheckInterval').value)||5;\n }\n return cfg;\n}\nasync function saveConfig(){\n const cfg = gatherConfig();\n if(!cfg) return;\n // Validate memory search prefix fields contain {content} if non-empty\n if(cfg.memory && cfg.memory.search){\n var pq = (cfg.memory.search.prefixQuery||'').trim();\n var pd = (cfg.memory.search.prefixDocument||'').trim();\n if(pq && pq.indexOf('{content}')===-1){ toast('Prefix Query must contain the {content} placeholder. Save aborted.','err'); return; }\n if(pd && pd.indexOf('{content}')===-1){ toast('Prefix Document must contain the {content} placeholder. Save aborted.','err'); return; }\n }\n // Block save if heartbeat is enabled but message is too short\n if(cfg.cron && cfg.cron.heartbeat && cfg.cron.heartbeat.enabled){\n var hbMsg = (cfg.cron.heartbeat.message||'').trim();\n if(hbMsg.length < 15){\n toast('Heartbeat message is too short (minimum 15 characters). Save aborted.','err');\n return;\n }\n }\n try{\n const res = await fetch(API+'/config',{method:'PUT',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify(cfg)});\n if(!res.ok){ const d=await res.json(); toast(d.error||'Save failed','err'); return; }\n currentConfig = await fetchAPI('/config');\n clearDirty();\n toast('Configuration saved','ok');\n updateConfigCheckPolling();\n // Re-render current section with protected values from server\n var activeSec = document.querySelector('.section.active');\n if(activeSec){\n var id = activeSec.id.replace('sec-','');\n if(id==='models') renderModelsTable();\n if(id==='vars') renderVarsTable();\n }\n }catch(e){ console.error('Save failed:',e); toast('Save failed: '+(e.message||e),'err'); }\n}\n"}
|
|
1
|
+
export function configJS(){return"\n/* ---- Env var ref markers ---- */\nfunction markEnvRefs(container){\n if(!container) return;\n container.querySelectorAll('input').forEach(function(inp){\n if(/^\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}$/.test(inp.value)){\n inp.classList.add('env-ref');\n inp.title='Stored as environment variable in .env';\n } else {\n inp.classList.remove('env-ref');\n inp.title='';\n }\n });\n}\n\n/* ---- STT ---- */\nasync function loadSTT(){\n currentConfig = await fetchAPI('/config');\n const s = currentConfig.stt||{};\n document.getElementById('sttEnabled').checked = !!s.enabled;\n document.getElementById('sttProvider').value = s.provider||'openai-whisper';\n const oai = s['openai-whisper']||{};\n populateSTTModelSelect();\n var sttModels = (currentConfig&¤tConfig.models)||[];\n document.getElementById('sttModelRef').value = typeof upgradeModelRef==='function' ? upgradeModelRef(oai.modelRef||'', sttModels) : (oai.modelRef||'');\n document.getElementById('sttOAILang').value = oai.language||'';\n const loc = s['local-whisper']||{};\n document.getElementById('sttLocalBin').value = loc.binaryPath||'whisper';\n document.getElementById('sttLocalModel').value = loc.model||'base';\n updateSTTFields();\n if(!loadSTT._bound){\n document.getElementById('sttProvider').addEventListener('change', updateSTTFields);\n loadSTT._bound = true;\n }\n sectionsLoaded.stt = true;\n}\nfunction updateSTTFields(){\n const prov = document.getElementById('sttProvider').value;\n document.getElementById('sttOpenAI').style.display = prov==='openai-whisper'?'':'none';\n document.getElementById('sttLocal').style.display = prov==='local-whisper'?'':'none';\n}\n\n/* ---- Memory ---- */\nasync function loadMemory(){\n currentConfig = await fetchAPI('/config');\n const m = currentConfig.memory||{};\n document.getElementById('memEnabled').checked = !!m.enabled;\n document.getElementById('memDir').value = m.dir||'./memory';\n document.getElementById('memStrategy').value = m.recallStrategy||'builtin-only';\n // Search settings\n const s = m.search||{};\n populateMemSearchModelSelect();\n var memModels = (currentConfig&¤tConfig.models)||[];\n document.getElementById('memSearchModelRef').value = typeof upgradeModelRef==='function' ? upgradeModelRef(s.modelRef||'', memModels) : (s.modelRef||'');\n document.getElementById('memSearchEmbModel').value = s.embeddingModel||'text-embedding-3-small';\n document.getElementById('memSearchPrefixQuery').value = s.prefixQuery||'';\n document.getElementById('memSearchPrefixDocument').value = s.prefixDocument||'';\n var dimsVal = String(s.embeddingDimensions||1536);\n document.getElementById('memSearchDims').value = dimsVal;\n document.getElementById('memSearchDimsValue').textContent = dimsVal;\n document.getElementById('memSearchMaxResults').value = s.maxResults||6;\n document.getElementById('memSearchDebounce').value = s.updateDebounceMs||3000;\n document.getElementById('memSearchEmbedInterval').value = s.embedIntervalMs||300000;\n document.getElementById('memSearchMaxSnippet').value = s.maxSnippetChars||700;\n document.getElementById('memSearchMaxInjected').value = s.maxInjectedChars??4000;\n document.getElementById('memSearchRrfK').value = s.rrfK||60;\n updateMemSearchFields();\n // L0 settings\n const l0 = m.l0||{};\n document.getElementById('l0Enabled').checked = l0.enabled !== false; // default true\n document.getElementById('l0Model').value = l0.model||'';\n updateL0Hint();\n document.getElementById('l0Model').addEventListener('input', updateL0Hint);\n // Intercept toggle-off\n document.getElementById('memEnabled').addEventListener('change', function(){\n if(!this.checked){\n this.checked = true; // revert until confirmed\n document.getElementById('memDisableModal').classList.add('open');\n }\n });\n sectionsLoaded.memory = true;\n}\nfunction updateMemSearchFields(){\n var strategy = document.getElementById('memStrategy').value;\n document.getElementById('memSearchSettings').style.display = strategy==='search'?'':'none';\n}\nfunction updateL0Hint(){\n var model = (document.getElementById('l0Model').value||'').trim();\n document.getElementById('l0OpenAIHint').style.display = model ? '' : 'none';\n}\nasync function testEmbedding(){\n var btn = document.getElementById('memSearchTestBtn');\n var result = document.getElementById('memSearchTestResult');\n btn.disabled = true;\n btn.textContent = 'Testing...';\n result.style.color = 'var(--text-muted)';\n result.textContent = '';\n try {\n var body = {\n modelRef: document.getElementById('memSearchModelRef').value,\n embeddingModel: document.getElementById('memSearchEmbModel').value || 'text-embedding-3-small',\n embeddingDimensions: parseInt(document.getElementById('memSearchDims').value) || 1536\n };\n var resp = await fetch(API+'/memory-search/test-embedding', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, credentials:'include', body:JSON.stringify(body)});\n var data = await resp.json();\n if(data.ok){\n result.style.color = 'var(--success, #22c55e)';\n result.textContent = '✓ OK — model: '+data.model+', dims: '+data.dimensions+', latency: '+data.latencyMs+'ms';\n } else {\n result.style.color = 'var(--error, #ef4444)';\n result.textContent = '✗ '+data.error;\n }\n } catch(err){\n result.style.color = 'var(--error, #ef4444)';\n result.textContent = '✗ Connection error: '+err.message;\n } finally {\n btn.disabled = false;\n btn.textContent = 'Test Embedding';\n }\n}\nfunction closeMemDisableModal(confirmed){\n document.getElementById('memDisableModal').classList.remove('open');\n if(confirmed){\n document.getElementById('memEnabled').checked = false;\n saveConfig();\n }\n}\n\n/* ---- Settings ---- */\nasync function loadSettings(){\n currentConfig = await fetchAPI('/config');\n document.getElementById('settingsHost').value = currentConfig.host || '127.0.0.1';\n document.getElementById('settingsUiPort').value = (currentConfig.nostromo && currentConfig.nostromo.port) || '3001';\n document.getElementById('settingsTimezone').value = currentConfig.timezone || '';\n document.getElementById('settingsAutoRestart').checked = !!(currentConfig.nostromo && currentConfig.nostromo.autoRestart);\n document.getElementById('settingsConfigCheckInterval').value = (currentConfig.nostromo && currentConfig.nostromo.configCheckInterval) || 5;\n sectionsLoaded.settings = true;\n}\n\n/* ---- Access key display ---- */\nvar _currentKey = '';\nvar _keyRevealed = false;\n\nfunction maskKey(key){\n // Show first 4 chars, mask the rest: \"ABCD-****-****-****\"\n if(key.length<=4) return key;\n return key.substring(0,4) + key.substring(4).replace(/[A-Za-z0-9]/g, '*');\n}\n\nasync function loadCurrentKey(){\n try{\n const res = await fetch(API+'/key');\n const data = await res.json();\n _currentKey = data.key || '';\n _keyRevealed = false;\n var row = document.getElementById('currentKeyRow');\n var el = document.getElementById('currentKeyValue');\n if(_currentKey && _currentKey !== '0000'){\n el.textContent = maskKey(_currentKey);\n row.style.display = 'flex';\n } else {\n row.style.display = 'none';\n }\n document.getElementById('keyEyeOff').style.display = '';\n document.getElementById('keyEyeOn').style.display = 'none';\n }catch(e){}\n}\n\nfunction toggleKeyVisibility(){\n _keyRevealed = !_keyRevealed;\n var el = document.getElementById('currentKeyValue');\n el.textContent = _keyRevealed ? _currentKey : maskKey(_currentKey);\n document.getElementById('keyEyeOff').style.display = _keyRevealed ? 'none' : '';\n document.getElementById('keyEyeOn').style.display = _keyRevealed ? '' : 'none';\n}\n\nasync function copyKey(){\n try{\n await navigator.clipboard.writeText(_currentKey);\n document.getElementById('keyCopyIcon').style.display = 'none';\n document.getElementById('keyCheckIcon').style.display = '';\n setTimeout(function(){\n document.getElementById('keyCopyIcon').style.display = '';\n document.getElementById('keyCheckIcon').style.display = 'none';\n }, 1500);\n }catch(e){ toast('Copy failed','err'); }\n}\n\n/* ---- Regenerate key ---- */\nfunction confirmRegenKey(){\n document.getElementById('regenKeyModal').classList.add('open');\n}\nfunction closeRegenKeyModal(){\n document.getElementById('regenKeyModal').classList.remove('open');\n}\nasync function regenKey(){\n try{\n const res = await fetch(API+'/key/regenerate',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n const data = await res.json();\n if(data.key){\n showNewKey(data.key);\n _currentKey = data.key;\n _keyRevealed = false;\n var el = document.getElementById('currentKeyValue');\n el.textContent = maskKey(data.key);\n document.getElementById('currentKeyRow').style.display = 'flex';\n document.getElementById('keyEyeOff').style.display = '';\n document.getElementById('keyEyeOn').style.display = 'none';\n }\n }catch(e){ toast('Failed','err'); }\n}\n\n/* ---- Save config ---- */\nfunction gatherConfig(){\n if(!currentConfig){ toast('Config not loaded yet — cannot save','err'); return null; }\n const cfg = JSON.parse(JSON.stringify(currentConfig));\n // Verbose debug logs\n var logVerboseEl = document.getElementById('logVerbose');\n if(logVerboseEl) cfg.verboseDebugLogs = logVerboseEl.checked;\n // Channels\n const wrap = document.getElementById('channelCards');\n if(wrap){\n if(!cfg.channels) cfg.channels={};\n for(const ch of CHANNEL_LIST){\n const toggle = wrap.querySelector('[data-ch-toggle=\"'+ch+'\"]');\n if(!toggle) continue;\n if(!cfg.channels[ch]) cfg.channels[ch]={};\n cfg.channels[ch].enabled = toggle.checked;\n }\n // Collect all channel fields via data-ch-field=\"channel.account.key\" or \"channel.key\"\n wrap.querySelectorAll('[data-ch-field]').forEach(inp=>{\n const parts = inp.dataset.chField.split('.');\n const chName = parts[0];\n if(!cfg.channels[chName]) cfg.channels[chName]={};\n if(parts.length === 3){\n // channel.account.key\n const [, acct, key] = parts;\n if(!cfg.channels[chName].accounts) cfg.channels[chName].accounts={};\n if(!cfg.channels[chName].accounts[acct]) cfg.channels[chName].accounts[acct]={};\n var val;\n if(key==='allowFrom') val = inp.value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n else if(inp.type==='number') val = parseInt(inp.value)||0;\n else val = inp.value;\n cfg.channels[chName].accounts[acct][key] = val;\n } else if(parts.length === 2){\n // channel.key (e.g. responses.port)\n const key = parts[1];\n cfg.channels[chName][key] = inp.type==='number' ? (parseInt(inp.value)||0) : inp.value;\n }\n });\n }\n // Models\n if(currentConfig&¤tConfig.models) cfg.models = currentConfig.models;\n // Agent — only gather from DOM if loadAgent() has populated the fields\n if(sectionsLoaded.agent){\n cfg.agent = cfg.agent||{};\n cfg.agent.model = document.getElementById('agentModel').value;\n cfg.agent.mainFallback = document.getElementById('agentMainFallback').value;\n cfg.agent.maxTurns = parseInt(document.getElementById('agentMaxTurns').value)||10;\n cfg.agent.permissionMode = document.getElementById('agentPermMode').value;\n cfg.agent.sessionTTL = parseInt(document.getElementById('agentSessionTTL').value)||3600;\n cfg.agent.settingSources = document.getElementById('agentSettingSources').value;\n cfg.agent.builtinCoderSkill = document.getElementById('agentCoderSkill').checked;\n cfg.agent.autoRenew = parseInt(document.getElementById('agentAutoRenew').value)||0;\n cfg.agent.allowedTools = [];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){if(cb.checked) cfg.agent.allowedTools.push(cb.dataset.tool);});\n cfg.agent.disallowedTools = [];\n cfg.agent.queueMode = document.getElementById('agentQueueMode').value;\n cfg.agent.queueDebounceMs = parseInt(document.getElementById('agentDebounceMs').value)||0;\n cfg.agent.queueCap = parseInt(document.getElementById('agentQueueCap').value)||0;\n cfg.agent.queueDropPolicy = document.getElementById('agentDropPolicy').value;\n cfg.agent.inflightTyping = document.getElementById('agentInflightTyping').checked;\n cfg.agent.autoApproveTools = document.getElementById('agentAutoApprove').checked;\n // Pico Agent\n cfg.agent.picoAgent = {\n enabled: document.getElementById('picoEnabled').checked,\n modelRefs: _picoModels.map(function(m){ return m.name + ':' + m.piProvider + ':' + m.piModelId; }),\n rollingMemoryModel: document.getElementById('picoRollingModel').value\n };\n delete cfg.agent.engine; // remove legacy\n }\n // Custom SubAgents (preserved from currentConfig, mutated in-place by the UI)\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n if(!cfg.agent) cfg.agent = {};\n cfg.agent.customSubAgents = currentConfig.agent.customSubAgents;\n }\n // Plugins (preserved from currentConfig, mutated in-place by the UI)\n if(currentConfig && currentConfig.agent && currentConfig.agent.plugins){\n if(!cfg.agent) cfg.agent = {};\n cfg.agent.plugins = currentConfig.agent.plugins;\n }\n // STT — only gather from DOM if loadSTT() has populated the fields\n if(sectionsLoaded.stt){\n cfg.stt = cfg.stt||{};\n cfg.stt.enabled = document.getElementById('sttEnabled').checked;\n cfg.stt.provider = document.getElementById('sttProvider').value;\n var sttModelName = document.getElementById('sttModelRef').value;\n cfg.stt['openai-whisper'] = {\n modelRef: sttModelName,\n model: 'whisper-1',\n language: document.getElementById('sttOAILang').value\n };\n cfg.stt['local-whisper'] = {\n binaryPath: document.getElementById('sttLocalBin').value,\n model: document.getElementById('sttLocalModel').value\n };\n }\n // TTS — only gather from DOM if loadTTS() has populated the fields\n if(sectionsLoaded.tts){\n cfg.tts = cfg.tts||{};\n cfg.tts.enabled = document.getElementById('ttsEnabled').checked;\n cfg.tts.provider = document.getElementById('ttsProvider').value;\n cfg.tts.maxTextLength = parseInt(document.getElementById('ttsMaxTextLength').value)||4096;\n cfg.tts.timeoutMs = parseInt(document.getElementById('ttsTimeoutMs').value)||30000;\n cfg.tts.edge = {\n voice: document.getElementById('ttsEdgeVoice').value || 'en-US-MichelleNeural'\n };\n cfg.tts.openai = {\n modelRef: document.getElementById('ttsOAIModelRef').value,\n model: document.getElementById('ttsOAIModel').value || 'gpt-4o-mini-tts',\n voice: document.getElementById('ttsOAIVoice').value || 'alloy'\n };\n cfg.tts.elevenlabs = {\n modelRef: document.getElementById('ttsELModelRef').value,\n voiceId: document.getElementById('ttsELVoiceId').value || 'pMsXgVXv3BLzUgSXRplE',\n modelId: document.getElementById('ttsELModelId').value || 'eleven_multilingual_v2'\n };\n }\n // Memory — only gather from DOM if loadMemory() has populated the fields\n if(sectionsLoaded.memory){\n cfg.memory = cfg.memory||{};\n cfg.memory.enabled = document.getElementById('memEnabled').checked;\n cfg.memory.dir = document.getElementById('memDir').value;\n cfg.memory.recallStrategy = document.getElementById('memStrategy').value;\n cfg.memory.search = {\n enabled: cfg.memory.recallStrategy === 'search',\n modelRef: document.getElementById('memSearchModelRef').value,\n embeddingModel: document.getElementById('memSearchEmbModel').value || 'text-embedding-3-small',\n prefixQuery: document.getElementById('memSearchPrefixQuery').value || '',\n prefixDocument: document.getElementById('memSearchPrefixDocument').value || '',\n embeddingDimensions: parseInt(document.getElementById('memSearchDims').value) || 1536,\n updateDebounceMs: parseInt(document.getElementById('memSearchDebounce').value) || 3000,\n embedIntervalMs: parseInt(document.getElementById('memSearchEmbedInterval').value) || 300000,\n maxResults: parseInt(document.getElementById('memSearchMaxResults').value) || 6,\n maxSnippetChars: parseInt(document.getElementById('memSearchMaxSnippet').value) || 700,\n maxInjectedChars: parseInt(document.getElementById('memSearchMaxInjected').value || '4000'),\n rrfK: parseInt(document.getElementById('memSearchRrfK').value) || 60\n };\n cfg.memory.l0 = {\n enabled: document.getElementById('l0Enabled').checked,\n model: (document.getElementById('l0Model').value||'').trim()\n };\n }\n // Cron — only gather from DOM if loadCron() has populated the fields\n if(sectionsLoaded.cron){\n cfg.cron = cfg.cron||{};\n cfg.cron.enabled = document.getElementById('cronEnabled').checked;\n cfg.cron.isolated = document.getElementById('cronIsolated').checked;\n cfg.cron.broadcastEvents = document.getElementById('cronBroadcast').checked;\n cfg.cron.heartbeat = cfg.cron.heartbeat||{};\n cfg.cron.heartbeat.enabled = document.getElementById('hbEnabled').checked;\n cfg.cron.heartbeat.channel = document.getElementById('hbChannel').value;\n cfg.cron.heartbeat.chatId = document.getElementById('hbChatId').value;\n cfg.cron.heartbeat.every = parseInt(document.getElementById('hbEvery').value)||1800000;\n cfg.cron.heartbeat.message = document.getElementById('hbMessage').value;\n cfg.cron.heartbeat.ackMaxChars = parseInt(document.getElementById('hbAckMaxChars').value)||300;\n }\n // Settings — only gather from DOM if loadSettings() has populated the fields\n if(sectionsLoaded.settings){\n cfg.host = document.getElementById('settingsHost').value || '127.0.0.1';\n cfg.timezone = document.getElementById('settingsTimezone').value || '';\n cfg.nostromo = cfg.nostromo||{};\n cfg.nostromo.port = parseInt(document.getElementById('settingsUiPort').value)||3001;\n cfg.nostromo.autoRestart = document.getElementById('settingsAutoRestart').checked;\n cfg.nostromo.configCheckInterval = parseInt(document.getElementById('settingsConfigCheckInterval').value)||5;\n }\n return cfg;\n}\nasync function saveConfig(){\n const cfg = gatherConfig();\n if(!cfg) return;\n // Validate memory search prefix fields contain {content} if non-empty\n if(cfg.memory && cfg.memory.search){\n var pq = (cfg.memory.search.prefixQuery||'').trim();\n var pd = (cfg.memory.search.prefixDocument||'').trim();\n if(pq && pq.indexOf('{content}')===-1){ toast('Prefix Query must contain the {content} placeholder. Save aborted.','err'); return; }\n if(pd && pd.indexOf('{content}')===-1){ toast('Prefix Document must contain the {content} placeholder. Save aborted.','err'); return; }\n }\n // Block save if heartbeat is enabled but message is too short\n if(cfg.cron && cfg.cron.heartbeat && cfg.cron.heartbeat.enabled){\n var hbMsg = (cfg.cron.heartbeat.message||'').trim();\n if(hbMsg.length < 15){\n toast('Heartbeat message is too short (minimum 15 characters). Save aborted.','err');\n return;\n }\n }\n try{\n const res = await fetch(API+'/config',{method:'PUT',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify(cfg)});\n if(!res.ok){ const d=await res.json(); toast(d.error||'Save failed','err'); return; }\n currentConfig = await fetchAPI('/config');\n clearDirty();\n toast('Configuration saved','ok');\n updateConfigCheckPolling();\n // Re-render current section with protected values from server\n var activeSec = document.querySelector('.section.active');\n if(activeSec){\n var id = activeSec.id.replace('sec-','');\n if(id==='models') renderModelsTable();\n if(id==='vars') renderVarsTable();\n }\n }catch(e){ console.error('Save failed:',e); toast('Save failed: '+(e.message||e),'err'); }\n}\n"}
|