@hera-al/server 1.6.28 → 1.6.29
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.
- package/dist/agent/session-db.d.ts +6 -0
- package/dist/agent/session-db.js +1 -1
- package/dist/agent/workspace-files.d.ts +11 -0
- package/dist/agent/workspace-files.js +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -1
- package/dist/gateway/channels/telegram/index.js +1 -1
- package/dist/memory/l0-generator.d.ts +39 -0
- package/dist/memory/l0-generator.js +1 -0
- package/dist/memory/memory-search.d.ts +11 -0
- package/dist/memory/memory-search.js +1 -1
- package/dist/nostromo/ui-html-layout.js +1 -1
- package/dist/nostromo/ui-js-config.js +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -1
- package/dist/tools/memory-tools.js +1 -1
- package/installationPkg/config.example.yaml +45 -13
- package/package.json +2 -2
|
@@ -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 // 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}\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 }\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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare class Server {
|
|
|
44
44
|
private stopMemorySearch;
|
|
45
45
|
private collectBroadcastTargets;
|
|
46
46
|
private executeCronJob;
|
|
47
|
+
private triageHeartbeat;
|
|
47
48
|
private setupCommands;
|
|
48
49
|
private registerChannels;
|
|
49
50
|
handleMessage(msg: IncomingMessage, isRetry?: boolean): Promise<string>;
|
package/dist/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as m}from"./gateway/channels/whatsapp.js";import{WebChatChannel as l}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as y,loadWorkspaceFiles as S}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as j,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as D}from"./commands/help.js";import{McpCommand as $}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as P}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as F}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{DebugA2UICommand as O}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as H}from"./cron/cron-service.js";import{stripHeartbeatToken as L,isHeartbeatContentEffectivelyEmpty as Q}from"./cron/heartbeat-token.js";import{createServerToolsServer as W}from"./tools/server-tools.js";import{createCronToolsServer as z}from"./tools/cron-tools.js";import{createTTSToolsServer as G}from"./tools/tts-tools.js";import{createMemoryToolsServer as q}from"./tools/memory-tools.js";import{createBrowserToolsServer as V}from"./tools/browser-tools.js";import{createPicoToolsServer as J}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as X}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Y}from"./tools/concept-tools.js";import{BrowserService as Z}from"./browser/browser-service.js";import{MemorySearch as ee}from"./memory/memory-search.js";import{ConceptStore as te}from"./memory/concept-store.js";import{stripMediaLines as se}from"./utils/media-response.js";import{loadConfig as ne,loadRawConfig as oe,backupConfig as ie,resolveModelEntry as re,modelRefName as ae}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as he}from"./utils/logger.js";import{SessionErrorHandler as ge}from"./agent/session-error-handler.js";import{initStickerCache as me}from"./gateway/channels/telegram/stickers.js";const le=he("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),me(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new Z,this.conceptStore=new te(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),m=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,m?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new H({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new ee(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),q(this.memorySearch);le.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return le.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=o(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(Q(n))return le.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=i?"cron":t.channel,a=i?t.name:t.chatId;le.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=L(h,e);if(s)return le.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();le.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g))),await Promise.allSettled(e.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g),await this.channelManager.releaseTyping(t.channel,t.chatId).catch(()=>{});return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:ae(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ae(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ae(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new x(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new $(()=>this.agentService.getToolServers())),this.commandRegistry.register(new D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){le.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new m(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new l),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";le.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&le.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/")&&this.agentService.isBusy(s))return"I'm busy right now. Please resend this request later.";const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=u(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId});le.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,c={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},h=S(this.config.dataDir),g={config:this.config,sessionContext:c,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},m=b(g),l=b({...g,mode:"minimal"});le.debug(`[${s}] System prompt (${m.length} chars): ${this.config.verboseDebugLogs?m:m.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,m,l,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=ge.analyzeError(r.error,r),c=ge.getRecoveryStrategy(a);return le.warn(`[${s}] ${c.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),c.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==c.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(le.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(le.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",se(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(le.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){le.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),le.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{le.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),le.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),le.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),le.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?le.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?le.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):le.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){le.info("Trigger restart requested");const e=ne();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();le.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),le.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){le.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(le.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>le.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){le.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){le.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}le.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){le.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),le.info("Server reconfigured successfully")}async stop(){le.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),le.info("Server stopped")}}
|
|
1
|
+
import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as y,loadWorkspaceFiles as S}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as R}from"./media/message-processor.js";import{loadSTTProvider as M}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as j,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as $}from"./commands/stop.js";import{HelpCommand as D}from"./commands/help.js";import{McpCommand as x}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as P}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as F}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{DebugA2UICommand as O}from"./commands/debuga2ui.js";import{DebugDynamicCommand as H}from"./commands/debugdynamic.js";import{CronService as B}from"./cron/cron-service.js";import{stripHeartbeatToken as L,isHeartbeatContentEffectivelyEmpty as Q}from"./cron/heartbeat-token.js";import{createServerToolsServer as W}from"./tools/server-tools.js";import{createCronToolsServer as z}from"./tools/cron-tools.js";import{createTTSToolsServer as G}from"./tools/tts-tools.js";import{createMemoryToolsServer as q}from"./tools/memory-tools.js";import{createBrowserToolsServer as V}from"./tools/browser-tools.js";import{createPicoToolsServer as J}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as X}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Y}from"./tools/concept-tools.js";import{BrowserService as Z}from"./browser/browser-service.js";import{MemorySearch as ee}from"./memory/memory-search.js";import{ConceptStore as te}from"./memory/concept-store.js";import{stripMediaLines as se}from"./utils/media-response.js";import{loadConfig as ne,loadRawConfig as oe,backupConfig as ie,resolveModelEntry as re,modelRefName as ae}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as he}from"./utils/logger.js";import{SessionErrorHandler as ge}from"./agent/session-error-handler.js";import{initStickerCache as le}from"./gateway/channels/telegram/stickers.js";const me=he("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=M(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,n),le(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new Z,this.conceptStore=new te(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new B({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new ee(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),q(this.memorySearch);me.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return me.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return me.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};me.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;me.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const i={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},r=await this.handleMessage(i);let a=r;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=L(r,t);if(s)return me.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:r,delivered:!1};a=n}if(t){const t=this.collectBroadcastTargets();me.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,a))),await Promise.allSettled(t.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(e.channel,e.chatId,a),await this.channelManager.releaseTyping(e.channel,e.chatId).catch(()=>{});return{response:a,delivered:!0}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const i=o(this.config.dataDir,"HEARTBEAT.md");let r=!1;if(n(i))try{const t=e(i,"utf-8");r=!Q(t)}catch{r=!0}const a=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?a?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:r||a?{shouldRun:!0,reason:a?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:ae(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ae(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ae(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new $(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getToolServers())),this.commandRegistry.register(new D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new H(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){me.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";me.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&me.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/")&&this.agentService.isBusy(s))return"I'm busy right now. Please resend this request later.";const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=u(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId});me.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,c={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},h=S(this.config.dataDir),g={config:this.config,sessionContext:c,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},l=b(g),m=b({...g,mode:"minimal"});me.debug(`[${s}] System prompt (${l.length} chars): ${this.config.verboseDebugLogs?l:l.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,l,m,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return me.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=ge.analyzeError(r.error,r),c=ge.getRecoveryStrategy(a);return me.warn(`[${s}] ${c.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),c.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==c.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(me.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(me.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",se(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(me.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(me.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){me.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),me.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{me.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),me.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),me.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),me.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?me.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?me.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):me.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){me.info("Trigger restart requested");const e=ne();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();me.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),me.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){me.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(me.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>me.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){me.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){me.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}me.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){me.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=M(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),me.info("Server reconfigured successfully")}async stop(){me.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),me.info("Server stopped")}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createSdkMcpServer as e,tool as
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as r}from"zod";import{createLogger as n}from"../utils/logger.js";const o=n("MemoryTools");export function createMemoryToolsServer(n){return e({name:"memory-tools",version:"1.0.0",tools:[t("memory_search","Search conversation memory using hybrid keyword + semantic search. Returns matching chunks from past conversations with relevance scores. Results include chunkId — use memory_expand to get full chunk content when snippets are insufficient. Formulate clear, specific queries. For broad searches, call multiple times with different angles or phrasings.",{query:r.string().describe("The search query — be specific and descriptive for best results"),maxResults:r.number().optional().describe("Maximum number of results to return (default 6)")},async e=>{try{const t=await n.search(e.query,e.maxResults);if(0===t.length)return{content:[{type:"text",text:"No matching memories found."}]};const r=n.getMaxInjectedChars();let s=JSON.stringify(t,null,2);if(r>0&&s.length>r){const e=[...t];for(;e.length>1&&(e.pop(),s=JSON.stringify(e,null,2),!(s.length<=r)););s.length>r&&(s=s.slice(0,r)+"\n... [truncated — result exceeded maxInjectedChars limit]"),o.info(`memory_search: trimmed from ${t.length} to ${e.length} results to fit maxInjectedChars=${r}`)}return{content:[{type:"text",text:s}]}}catch(e){const t=e instanceof Error?e.message:String(e);return o.error(`memory_search error: ${t}`),{content:[{type:"text",text:`Search error: ${t}`}],isError:!0}}}),t("memory_expand","Get the full content of one or more memory chunks by their IDs (from memory_search results). Use this when a search snippet or L0 abstract is too short and you need the complete text.",{ids:r.array(r.number()).describe("Array of chunk IDs from memory_search results (chunkId field)")},async e=>{try{const t=n.expandChunks(e.ids);return 0===t.length?{content:[{type:"text",text:"No chunks found for the given IDs."}]}:{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return o.error(`memory_expand error: ${t}`),{content:[{type:"text",text:`Expand error: ${t}`}],isError:!0}}}),t("memory_get","Read a memory file (full or a specific slice) to get surrounding context after a search hit. Use the path from memory_search results.",{path:r.string().describe("Relative path to the memory file (from memory_search results)"),from:r.number().optional().describe("Starting line number (1-based). Omit to read from the beginning."),lines:r.number().optional().describe("Number of lines to read. Omit to read the entire file.")},async e=>{try{const t=n.readFile(e.path,e.from,e.lines);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return o.error(`memory_get error: ${t}`),{content:[{type:"text",text:`Read error: ${t}`}],isError:!0}}})]})}
|
|
@@ -6,7 +6,7 @@ gmabPath: "~/gmab" # root data directory (expanded at run
|
|
|
6
6
|
host: "127.0.0.1" # bind address for HTTP servers
|
|
7
7
|
logLevel: "info" # debug | info | warn | error
|
|
8
8
|
verboseDebugLogs: true # show full prompt/response text in debug logs (false = truncate to 15 chars)
|
|
9
|
-
timezone: "Europe/Rome"
|
|
9
|
+
timezone: "Europe/Rome" # auto-detected on first boot (e.g. Europe/Rome, America/New_York)
|
|
10
10
|
fastProxyUrl: "http://localhost:4181" # Anthropic Tool Name proxy URL (atn-proxy)
|
|
11
11
|
|
|
12
12
|
channels:
|
|
@@ -72,7 +72,7 @@ channels:
|
|
|
72
72
|
# accounts:
|
|
73
73
|
# default:
|
|
74
74
|
# authDir: "./data/whatsapp"
|
|
75
|
-
# dmPolicy: "
|
|
75
|
+
# dmPolicy: "allowlist"
|
|
76
76
|
# allowFrom: []
|
|
77
77
|
|
|
78
78
|
# discord:
|
|
@@ -130,7 +130,8 @@ channels:
|
|
|
130
130
|
|
|
131
131
|
models: # model registry
|
|
132
132
|
# ─── Internal models (Anthropic, via SDK) ──────────────────────
|
|
133
|
-
|
|
133
|
+
# Model IDs follow Anthropic naming. Append [1m] for 1M context window.
|
|
134
|
+
- id: claude-opus-4-6[1m]
|
|
134
135
|
name: Claude Opus
|
|
135
136
|
types: [internal] # internal | external | env-var
|
|
136
137
|
- id: claude-sonnet-4-6
|
|
@@ -152,6 +153,19 @@ models: # model registry
|
|
|
152
153
|
costOutput: 0
|
|
153
154
|
costCacheRead: 0
|
|
154
155
|
costCacheWrite: 0
|
|
156
|
+
|
|
157
|
+
# - id: xAI
|
|
158
|
+
# name: xAI Grok
|
|
159
|
+
# types: [external]
|
|
160
|
+
# apiKey: "${XAI_API_KEY}"
|
|
161
|
+
# baseURL: https://api.x.ai/v1
|
|
162
|
+
# useEnvVar: XAI_API_KEY
|
|
163
|
+
# contextWindow: 200000
|
|
164
|
+
# costInput: 0
|
|
165
|
+
# costOutput: 0
|
|
166
|
+
# costCacheRead: 0
|
|
167
|
+
# costCacheWrite: 0
|
|
168
|
+
|
|
155
169
|
- id: embeddinggemma
|
|
156
170
|
name: Embedding Gemma
|
|
157
171
|
types: [external]
|
|
@@ -185,7 +199,7 @@ models: # model registry
|
|
|
185
199
|
useEnvVar: OPENROUTER_API_KEY
|
|
186
200
|
|
|
187
201
|
# ─── OpenRouter models (external, via openrouter.ai) ───────────
|
|
188
|
-
- id: "openrouter:openai/gpt-5.
|
|
202
|
+
- id: "openrouter:openai/gpt-5.4"
|
|
189
203
|
name: OpenRouter GPT
|
|
190
204
|
types: [external]
|
|
191
205
|
apiKey: "${OPENROUTER_API_KEY}"
|
|
@@ -245,6 +259,16 @@ models: # model registry
|
|
|
245
259
|
costOutput: 0
|
|
246
260
|
costCacheRead: 0
|
|
247
261
|
costCacheWrite: 0
|
|
262
|
+
- id: "openai/gpt-4.1-mini"
|
|
263
|
+
name: OpenRouter Mnemosyne
|
|
264
|
+
types: [external]
|
|
265
|
+
apiKey: "${OPENROUTER_API_KEY}"
|
|
266
|
+
baseURL: https://openrouter.ai/api/v1
|
|
267
|
+
contextWindow: 200000
|
|
268
|
+
costInput: 0
|
|
269
|
+
costOutput: 0
|
|
270
|
+
costCacheRead: 0
|
|
271
|
+
costCacheWrite: 0
|
|
248
272
|
|
|
249
273
|
stt:
|
|
250
274
|
enabled: true
|
|
@@ -287,27 +311,35 @@ memory:
|
|
|
287
311
|
embeddingDimensions: 768 # 512, 768, or 1536 (native) — lower = faster, higher = more precise
|
|
288
312
|
updateDebounceMs: 3000 # debounce for fs.watch before re-indexing
|
|
289
313
|
embedIntervalMs: 300000 # interval (ms) between embedding cycles (5 min)
|
|
290
|
-
maxResults: 10
|
|
314
|
+
maxResults: 10 # default max results returned by memory_search
|
|
291
315
|
maxSnippetChars: 700 # max chars per snippet in results
|
|
292
316
|
maxInjectedChars: 4000 # max total chars injected into context from search results
|
|
293
317
|
rrfK: 60 # RRF fusion constant (higher = smoother blending)
|
|
318
|
+
l0:
|
|
319
|
+
enabled: true # generate L0 abstracts for memory chunks
|
|
320
|
+
# model: gpt-4o-mini # if empty/missing → uses Haiku; set for OpenAI model
|
|
321
|
+
items:
|
|
322
|
+
enabled: false # extract structured items (people, events, etc.) from conversations
|
|
323
|
+
extractionModelRef: "OpenRouter Mnemosyne" # model used for extraction
|
|
324
|
+
dir: ./memory
|
|
294
325
|
|
|
295
326
|
agent:
|
|
296
|
-
model: "Claude Opus"
|
|
297
|
-
mainFallback: "" # fallback model if primary fails (
|
|
327
|
+
model: "Claude Opus:claude-opus-4-6[1m]" # model name:id from the registry. Append [1m] for 1M context.
|
|
328
|
+
mainFallback: "" # fallback model if primary fails (format: "Name:id")
|
|
298
329
|
# engine — agent execution engine
|
|
299
330
|
engine:
|
|
300
331
|
type: "claudecode" # claudecode | pi
|
|
301
|
-
piModelRef: "" # for pi engine: "Name:provider:modelId" (e.g. "GPT:openrouter:openai/gpt-5.
|
|
332
|
+
piModelRef: "" # for pi engine: "Name:provider:modelId" (e.g. "GPT:openrouter:openai/gpt-5.4")
|
|
302
333
|
# picoAgent — use external LLM providers (OpenRouter, OpenAI, etc.) via pi-ai instead of Claude SDK
|
|
303
334
|
picoAgent:
|
|
304
335
|
enabled: true
|
|
305
336
|
modelRefs: # list of "Name:provider:modelId" refs
|
|
306
|
-
- "
|
|
307
|
-
- "
|
|
308
|
-
- "Grok:openrouter:x-ai/grok-4.1-fast"
|
|
309
|
-
- "
|
|
310
|
-
|
|
337
|
+
- "OpenRouter GPT:openrouter:openai/gpt-5.4"
|
|
338
|
+
- "OpenRouter Flash:openrouter:google/gemini-3-flash-preview"
|
|
339
|
+
- "OpenRouter Grok:openrouter:x-ai/grok-4.1-fast"
|
|
340
|
+
- "OpenRouter GeminiPro:openrouter:google/gemini-3-pro-preview"
|
|
341
|
+
- "OpenRouter Opus1m:openrouter:anthropic/claude-opus-4.6"
|
|
342
|
+
rollingMemoryModel: "OpenRouter Flash:openrouter:google/gemini-3-flash-preview"
|
|
311
343
|
maxTurns: 300
|
|
312
344
|
permissionMode: "bypassPermissions"
|
|
313
345
|
sessionTTL: 3600
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hera-al/server",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.29",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@a2ui/lit": "^0.8.1",
|
|
66
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.72",
|
|
66
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.72",
|
|
67
67
|
"@clack/prompts": "^1.0.0",
|
|
68
68
|
"@grammyjs/runner": "^2.0.3",
|
|
69
69
|
"@hera-al/browser-server": "^1.0.5",
|