@hera-al/server 1.6.6 → 1.6.11

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.
Files changed (56) hide show
  1. package/dist/agent/agent-service.d.ts +1 -0
  2. package/dist/agent/agent-service.js +1 -1
  3. package/dist/agent/session-agent.d.ts +4 -2
  4. package/dist/agent/session-agent.js +1 -1
  5. package/dist/agent/session-db.d.ts +2 -0
  6. package/dist/agent/session-db.js +1 -1
  7. package/dist/commands/command.d.ts +7 -0
  8. package/dist/commands/help.d.ts +1 -1
  9. package/dist/commands/help.js +1 -1
  10. package/dist/commands/models.d.ts +1 -1
  11. package/dist/commands/models.js +1 -1
  12. package/dist/config.d.ts +170 -1
  13. package/dist/config.js +1 -1
  14. package/dist/cron/cron-service.d.ts +36 -2
  15. package/dist/cron/cron-service.js +1 -1
  16. package/dist/cron/types.d.ts +6 -0
  17. package/dist/gateway/bridge.d.ts +1 -1
  18. package/dist/gateway/channel-manager.d.ts +1 -1
  19. package/dist/gateway/channel-manager.js +1 -1
  20. package/dist/gateway/channels/telegram/config-types.d.ts +93 -0
  21. package/dist/gateway/channels/telegram/config-types.js +1 -0
  22. package/dist/gateway/channels/telegram/edit-delete.d.ts +73 -0
  23. package/dist/gateway/channels/telegram/edit-delete.js +1 -0
  24. package/dist/gateway/channels/telegram/error-handling.d.ts +63 -0
  25. package/dist/gateway/channels/telegram/error-handling.js +1 -0
  26. package/dist/gateway/channels/{telegram.d.ts → telegram/index.d.ts} +23 -11
  27. package/dist/gateway/channels/telegram/index.js +1 -0
  28. package/dist/gateway/channels/telegram/inline-buttons.d.ts +60 -0
  29. package/dist/gateway/channels/telegram/inline-buttons.js +1 -0
  30. package/dist/gateway/channels/telegram/polls.d.ts +50 -0
  31. package/dist/gateway/channels/telegram/polls.js +1 -0
  32. package/dist/gateway/channels/telegram/reactions.d.ts +56 -0
  33. package/dist/gateway/channels/telegram/reactions.js +1 -0
  34. package/dist/gateway/channels/telegram/retry-policy.d.ts +28 -0
  35. package/dist/gateway/channels/telegram/retry-policy.js +1 -0
  36. package/dist/gateway/channels/telegram/send.d.ts +55 -0
  37. package/dist/gateway/channels/telegram/send.js +1 -0
  38. package/dist/gateway/channels/telegram/stickers.d.ts +96 -0
  39. package/dist/gateway/channels/telegram/stickers.js +1 -0
  40. package/dist/gateway/channels/telegram/thread-support.d.ts +99 -0
  41. package/dist/gateway/channels/telegram/thread-support.js +1 -0
  42. package/dist/gateway/channels/telegram/utils.d.ts +69 -0
  43. package/dist/gateway/channels/telegram/utils.js +1 -0
  44. package/dist/gateway/channels/webchat.d.ts +1 -1
  45. package/dist/gateway/channels/webchat.js +1 -1
  46. package/dist/nostromo/ui-js-agent.js +1 -1
  47. package/dist/pi-agent-provider/pi-query.js +1 -1
  48. package/dist/pi-agent-provider/pi-types.d.ts +4 -0
  49. package/dist/server.js +1 -1
  50. package/dist/tools/cron-tools.js +1 -1
  51. package/dist/tools/message-tools.js +1 -1
  52. package/dist/tools/telegram-actions-tools.d.ts +13 -0
  53. package/dist/tools/telegram-actions-tools.js +1 -0
  54. package/installationPkg/config.example.yaml +55 -1
  55. package/package.json +1 -1
  56. package/dist/gateway/channels/telegram.js +0 -1
@@ -1 +1 @@
1
- export function agentJS(){return"\nvar SA_TOOL_LIST = ['Read','Write','Edit','Bash','Glob','Grep','WebSearch','WebFetch'];\n\nvar _editModelIdx = -1;\n\n/* ---- Models ---- */\nfunction showAddModel(){\n document.getElementById('addModelForm').style.display='';\n // Reset form\n document.getElementById('newModelId').value='';\n document.getElementById('newModelName').value='';\n document.getElementById('newModelBaseURL').value='https://api.openai.com/v1';\n document.getElementById('newModelApiKey').value='';\n document.getElementById('newModelEnvVar').value='';\n document.getElementById('newModelType').value='external';\n document.getElementById('newModelProxy').value='not-used';\n document.getElementById('newModelFastUrl').value='';\n document.getElementById('newModelFastProxyApiKey').value='';\n document.getElementById('newModelContextWindow').value='200000';\n document.getElementById('newModelCostInput').value='0';\n document.getElementById('newModelCostOutput').value='0';\n document.getElementById('newModelCostCacheRead').value='0';\n document.getElementById('newModelCostCacheWrite').value='0';\n updateNewModelApiFields();\n}\nfunction hideAddModel(){ document.getElementById('addModelForm').style.display='none'; }\nfunction sanitizeEnvVarInput(el){\n var v = el.value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'');\n if(v !== el.value) el.value = v;\n}\nfunction updateNewModelApiFields(){\n var type = document.getElementById('newModelType').value;\n document.getElementById('newModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('newModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('newModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('newModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateNewModelProxyFields();\n}\nfunction updateNewModelProxyFields(){\n var proxy = document.getElementById('newModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('newModelFastUrl');\n var fk = document.getElementById('newModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nfunction updateEditModelApiFields(){\n var type = document.getElementById('editModelType').value;\n document.getElementById('editModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('editModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('editModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('editModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateEditModelProxyFields();\n}\nfunction updateEditModelProxyFields(){\n var proxy = document.getElementById('editModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('editModelFastUrl');\n var fk = document.getElementById('editModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nasync function loadModels(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderModelsTable();\n}\nfunction renderModelsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('modelsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')!==-1) continue;\n hasVisible = true;\n var typeBadges = types.map(function(t){ return '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px;margin-right:2px\">'+esc(t)+'</span>'; }).join('');\n var proxyBadge = (m.proxy && m.proxy !== 'not-used') ? ' <span class=\"badge badge-green\" style=\"font-size:11px;padding:1px 6px\">'+esc(m.proxy)+'</span>' : '';\n tbody.innerHTML += '<tr data-model-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(m.id)+'</td><td>'+typeBadges+proxyBadge+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditModel('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteModel('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"4\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No models in registry</td></tr>';\n }\n // Hide edit form when re-rendering\n document.getElementById('editModelForm').style.display='none';\n _editModelIdx = -1;\n}\nfunction addModel(){\n var id = document.getElementById('newModelId').value.trim();\n var name = document.getElementById('newModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('newModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('newModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('newModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('newModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('newModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('newModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('newModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('newModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheWrite').value)||0) : 0;\n if(!currentConfig.models) currentConfig.models=[];\n var dup = currentConfig.models.some(function(m){ return m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models.push({id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite});\n hideAddModel();\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nvar _deleteModelIdx = -1;\nfunction confirmDeleteModel(idx){\n _deleteModelIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this model';\n document.getElementById('modelDeleteName').textContent = name;\n document.getElementById('modelDeleteModal').classList.add('open');\n}\nfunction closeModelDeleteModal(){\n document.getElementById('modelDeleteModal').classList.remove('open');\n _deleteModelIdx = -1;\n}\nfunction doDeleteModel(){\n if(_deleteModelIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteModelIdx,1);\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n }\n closeModelDeleteModal();\n}\nfunction startEditModel(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editModelIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editModelId').value = m.id||'';\n document.getElementById('editModelName').value = m.name||'';\n document.getElementById('editModelBaseURL').value = m.baseURL||'';\n document.getElementById('editModelApiKey').value = m.apiKey||'';\n document.getElementById('editModelEnvVar').value = m.useEnvVar||'';\n document.getElementById('editModelProxy').value = m.proxy||'not-used';\n document.getElementById('editModelFastUrl').value = m.fastUrl||'';\n document.getElementById('editModelFastProxyApiKey').value = m.fastProxyApiKey||'';\n document.getElementById('editModelContextWindow').value = m.contextWindow||200000;\n document.getElementById('editModelCostInput').value = m.costInput||0;\n document.getElementById('editModelCostOutput').value = m.costOutput||0;\n document.getElementById('editModelCostCacheRead').value = m.costCacheRead||0;\n document.getElementById('editModelCostCacheWrite').value = m.costCacheWrite||0;\n var types = m.types||['external'];\n document.getElementById('editModelType').value = types[0]||'external';\n updateEditModelApiFields();\n // Position edit form after the table\n document.getElementById('editModelForm').style.display='';\n}\nfunction finishEditModel(){\n if(_editModelIdx<0 || !currentConfig.models||!currentConfig.models[_editModelIdx]) return;\n var id = document.getElementById('editModelId').value.trim();\n var name = document.getElementById('editModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('editModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('editModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('editModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('editModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('editModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('editModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('editModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('editModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheWrite').value)||0) : 0;\n var dup = currentConfig.models.some(function(m, i){ return i!==_editModelIdx && m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models[_editModelIdx] = {id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite};\n _editModelIdx = -1;\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nfunction cancelEditModel(){\n _editModelIdx = -1;\n document.getElementById('editModelForm').style.display='none';\n}\nfunction populateModelSelects(){\n var models = (currentConfig&&currentConfig.models)||[];\n var picoEnabled = document.getElementById('picoEnabled') && document.getElementById('picoEnabled').checked;\n var picoNames = {};\n if (picoEnabled) { for (var pi=0; pi<_picoModels.length; pi++) picoNames[_picoModels[pi].name] = true; }\n var internalModels = models.filter(function(m){ var t=m.types||['external']; return t.indexOf('internal')!==-1 || (t.indexOf('external')!==-1 && m.proxy && m.proxy!=='not-used') || (picoEnabled && t.indexOf('external')!==-1 && picoNames[m.name]); });\n var selects = {\n agentModel: {val:'', allowNone:false},\n agentMainFallback: {val:'', allowNone:true}\n };\n for(var key in selects){\n var el = document.getElementById(key);\n if(!el) continue;\n selects[key].val = el.value;\n el.innerHTML='';\n if(selects[key].allowNone) el.innerHTML += '<option value=\"\">None</option>';\n for(var i=0;i<internalModels.length;i++){\n el.innerHTML += '<option value=\"'+esc(internalModels[i].name+':'+internalModels[i].id)+'\">'+esc(internalModels[i].name)+' ('+esc(internalModels[i].id)+')</option>';\n }\n el.value = selects[key].val;\n }\n populateSTTModelSelect();\n populateMemSearchModelSelect();\n populatePicoModelSelect();\n populateRollingModelSelect();\n}\nfunction populateSTTModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('sttModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\nfunction populateMemSearchModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('memSearchModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\n\n/* ---- Pico Agent ---- */\nvar _picoModels = []; // [{name, piProvider, piModelId, contextWindow}]\nvar _agentLoading = false; // suppress markDirty during initial load\n\nfunction detectPiProvider(model) {\n var url = (model.baseURL || '').toLowerCase();\n if (url.includes('openrouter.ai')) return 'openrouter';\n if (url.includes('openai.com')) return 'openai';\n if (url.includes('x.ai')) return 'xai';\n if (url.includes('googleapis.com')) return 'google';\n if (url.includes('groq.com')) return 'groq';\n if (url.includes('mistral.ai')) return 'mistral';\n return 'openai';\n}\n\nfunction loadPicoAgent() {\n var a = (currentConfig && currentConfig.agent) || {};\n var pico = a.picoAgent || {};\n // Fallback: migrate from engine if picoAgent absent and engine.type === \"pi\"\n if (!a.picoAgent && a.engine && a.engine.type === 'pi') {\n pico = { enabled: true, modelRefs: [], rollingMemoryModel: '' };\n if (a.engine.piModelRef) {\n pico.modelRefs = [a.engine.piModelRef];\n }\n }\n var el = document.getElementById('picoEnabled');\n if (el) el.checked = !!pico.enabled;\n // Parse modelRefs into _picoModels\n _picoModels = [];\n var refs = pico.modelRefs || [];\n var models = (currentConfig && currentConfig.models) || [];\n for (var i = 0; i < refs.length; i++) {\n var parts = refs[i].split(':');\n var name = parts[0] || '';\n var piProvider = parts.length >= 3 ? parts[1] : '';\n var piModelId = parts.length >= 3 ? parts.slice(2).join(':') : (parts[1] || '');\n // Find matching model entry for contextWindow\n var matched = models.filter(function(m){ return m.name === name; })[0];\n var ctx = (matched && matched.contextWindow) || 200000;\n if (!piProvider && matched) piProvider = detectPiProvider(matched);\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n }\n updatePicoFields();\n renderPicoModelList();\n // Set rolling model AFTER populateRollingModelSelect() has built the options\n var rollingEl = document.getElementById('picoRollingModel');\n if (rollingEl) rollingEl.value = pico.rollingMemoryModel || '';\n}\n\nfunction updatePicoFields() {\n var enabled = document.getElementById('picoEnabled').checked;\n var fields = document.getElementById('picoFields');\n if (fields) fields.style.display = enabled ? '' : 'none';\n if (enabled) {\n populatePicoModelSelect();\n populateRollingModelSelect();\n }\n populateModelSelects();\n if (!_agentLoading) markDirty();\n}\n\nfunction populatePicoModelSelect() {\n var models = (currentConfig && currentConfig.models) || [];\n var el = document.getElementById('picoModelSelect');\n if (!el) return;\n el.innerHTML = '<option value=\"\" disabled selected>Select a model...</option>';\n var alreadyAdded = {};\n for (var j = 0; j < _picoModels.length; j++) alreadyAdded[_picoModels[j].name] = true;\n var hasOptions = false;\n for (var i = 0; i < models.length; i++) {\n var types = models[i].types || ['external'];\n if (types.indexOf('external') === -1) continue;\n if (alreadyAdded[models[i].name]) continue;\n el.innerHTML += '<option value=\"' + esc(models[i].name) + '\">' + esc(models[i].name) + ' (' + esc(models[i].id) + ')</option>';\n hasOptions = true;\n }\n if (!hasOptions) {\n el.innerHTML = '<option value=\"\" disabled selected>-- no available models --</option>';\n }\n}\n\nfunction populateRollingModelSelect() {\n var models = (currentConfig && currentConfig.models) || [];\n var el = document.getElementById('picoRollingModel');\n if (!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- none --</option>';\n for (var i = 0; i < models.length; i++) {\n var types = models[i].types || ['external'];\n if (types.indexOf('external') === -1) continue;\n el.innerHTML += '<option value=\"' + esc(models[i].name) + '\">' + esc(models[i].name) + ' (' + esc(models[i].id) + ')</option>';\n }\n el.value = prev;\n}\n\nfunction addPicoModel() {\n var sel = document.getElementById('picoModelSelect');\n var name = sel.value;\n if (!name) return;\n var models = (currentConfig && currentConfig.models) || [];\n var matched = models.filter(function(m){ return m.name === name; })[0];\n if (!matched) return;\n var piProvider = detectPiProvider(matched);\n var piModelId = matched.id || '';\n // Strip registry namespace prefix (e.g. \"openrouter:openai/gpt-5.2\" → \"openai/gpt-5.2\")\n var nsIdx = piModelId.indexOf(':');\n if (nsIdx > 0) piModelId = piModelId.substring(nsIdx + 1);\n var ctx = matched.contextWindow || 200000;\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction removePicoModel(idx) {\n _picoModels.splice(idx, 1);\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction clearRollingModel() {\n var el = document.getElementById('picoRollingModel');\n if (el) el.value = '';\n markDirty();\n}\n\nfunction renderPicoModelList() {\n var container = document.getElementById('picoModelList');\n if (!container) return;\n if (_picoModels.length === 0) {\n container.innerHTML = '<div style=\"color:var(--text-muted);font-size:13px;padding:8px 0\">No models added. Use the select above to add models.</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < _picoModels.length; i++) {\n var m = _picoModels[i];\n var ctxLabel = m.contextWindow >= 1000 ? Math.round(m.contextWindow / 1000) + 'k ctx' : m.contextWindow + ' ctx';\n html += '<div class=\"pico-model-item\" draggable=\"true\" data-pico-idx=\"' + i + '\">';\n html += '<span class=\"pico-drag-handle\">⠇</span>';\n html += '<span class=\"pico-model-name\">' + esc(m.name) + '</span>';\n html += '<span class=\"pico-model-ctx\">' + esc(ctxLabel) + '</span>';\n if (i === 0) html += '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px\">default</span>';\n html += '<button class=\"pico-model-remove\" onclick=\"removePicoModel(' + i + ')\">&times;</button>';\n html += '</div>';\n }\n container.innerHTML = html;\n // Attach drag events\n var items = container.querySelectorAll('.pico-model-item');\n items.forEach(function(item) {\n item.addEventListener('dragstart', function(e) {\n e.dataTransfer.setData('text/plain', item.dataset.picoIdx);\n item.classList.add('dragging');\n });\n item.addEventListener('dragend', function() {\n item.classList.remove('dragging');\n });\n item.addEventListener('dragover', function(e) {\n e.preventDefault();\n });\n item.addEventListener('drop', function(e) {\n e.preventDefault();\n var fromIdx = parseInt(e.dataTransfer.getData('text/plain'));\n var toIdx = parseInt(item.dataset.picoIdx);\n if (fromIdx === toIdx) return;\n var moved = _picoModels.splice(fromIdx, 1)[0];\n _picoModels.splice(toIdx, 0, moved);\n renderPicoModelList();\n markDirty();\n });\n });\n}\n\n/* ---- Model ref upgrade ---- */\nfunction upgradeModelRef(val, models){\n if(!val) return val;\n if(val.indexOf(':')!==-1) return val;\n var m = models.filter(function(x){ return x.name===val; })[0];\n return m ? m.name+':'+m.id : val;\n}\n\n/* ---- Agent ---- */\nasync function loadAgent(){\n currentConfig = await fetchAPI('/config');\n const a = currentConfig.agent||{};\n var models = currentConfig.models||[];\n // Load pico agent FIRST so _picoModels is populated before populateModelSelects()\n _agentLoading = true;\n loadPicoAgent();\n populateModelSelects();\n document.getElementById('agentModel').value = upgradeModelRef(a.model||'', models);\n document.getElementById('agentMainFallback').value = upgradeModelRef(a.mainFallback||'', models);\n document.getElementById('agentMaxTurns').value = a.maxTurns||10;\n document.getElementById('agentPermMode').value = a.permissionMode||'default';\n document.getElementById('agentSessionTTL').value = a.sessionTTL||3600;\n document.getElementById('agentSettingSources').value = a.settingSources||'project';\n document.getElementById('agentCoderSkill').checked = !!a.builtinCoderSkill;\n document.getElementById('agentAutoRenew').value = a.autoRenew||0;\n var allowed = a.allowedTools||[];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){cb.checked = allowed.indexOf(cb.dataset.tool)!==-1;});\n document.getElementById('agentQueueMode').value = a.queueMode||'collect';\n document.getElementById('agentDebounceMs').value = a.queueDebounceMs!=null ? a.queueDebounceMs : 1500;\n document.getElementById('agentQueueCap').value = a.queueCap!=null ? a.queueCap : 20;\n document.getElementById('agentDropPolicy').value = a.queueDropPolicy||'summarize';\n document.getElementById('agentInflightTyping').checked = a.inflightTyping!==false;\n document.getElementById('agentAutoApprove').checked = a.autoApproveTools!==false;\n updateQueueFields();\n _agentLoading = false;\n sectionsLoaded.agent = true;\n}\n\n/* ---- Queue fields visibility ---- */\nfunction updateQueueFields(){\n var mode = document.getElementById('agentQueueMode').value;\n document.getElementById('queueCollectFields').style.display = mode==='collect' ? '' : 'none';\n}\n\n/* ---- SubAgents ---- */\nvar _saDeleteIdx = -1;\nasync function loadSubAgents(){\n currentConfig = await fetchAPI('/config');\n renderSubAgentCards();\n}\nfunction toggleSaAccordion(idx){\n var el = document.querySelector('.sa-acc[data-sa-idx=\"'+idx+'\"]');\n if(el) el.classList.toggle('open');\n}\nfunction renderSubAgentCards(){\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var container = document.getElementById('subAgentCards');\n var empty = document.getElementById('subAgentEmpty');\n if(sas.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n return;\n }\n empty.style.display = 'none';\n var order = [];\n for(var k=0;k<sas.length;k++) order.push(k);\n order.sort(function(a,b){ return (sas[a].name||'').localeCompare(sas[b].name||''); });\n var html = '';\n for(var oi = 0; oi < order.length; oi++){\n var i = order[oi];\n var sa = sas[i];\n html += '<div class=\"sa-acc\" data-sa-idx=\"'+i+'\">';\n html += '<div class=\"sa-acc-header\" onclick=\"toggleSaAccordion('+i+')\">';\n html += '<svg class=\"sa-acc-chevron\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>';\n html += '<span class=\"sa-acc-name\">' + esc(sa.name) + '</span>';\n if(sa.expandContext) html += '<span class=\"badge badge-blue\" style=\"font-size:10px;padding:1px 6px\">expanded</span>';\n html += '<label class=\"toggle\" onclick=\"event.stopPropagation()\"><input type=\"checkbox\" data-sa-toggle=\"'+i+'\" '+(sa.enabled?'checked':'')+'><span></span></label>';\n html += '<button class=\"btn-danger btn-sm\" onclick=\"event.stopPropagation();confirmDeleteSubAgent('+i+')\">Delete</button>';\n html += '</div>';\n html += '<div class=\"sa-acc-body\">';\n html += '<div class=\"field\"><label>Description</label><textarea data-sa-field=\"'+i+'.description\" rows=\"2\" oninput=\"updateSaField('+i+',&quot;description&quot;,this.value)\">'+esc(sa.description)+'</textarea></div>';\n html += '<div class=\"field\"><label>Prompt</label><textarea data-sa-field=\"'+i+'.prompt\" rows=\"3\" oninput=\"updateSaField('+i+',&quot;prompt&quot;,this.value)\">'+esc(sa.prompt)+'</textarea></div>';\n html += '<div class=\"field\"><label>Model</label><select data-sa-field=\"'+i+'.model\" onchange=\"updateSaField('+i+',&quot;model&quot;,this.value)\">';\n var saModels = ['inherit','sonnet','opus','haiku'];\n for(var j=0;j<saModels.length;j++){\n html += '<option value=\"'+saModels[j]+'\"'+(sa.model===saModels[j]?' selected':'')+'>'+saModels[j]+'</option>';\n }\n html += '</select></div>';\n var saTools = sa.tools||[];\n html += '<div class=\"field\"><label>Tools</label><div style=\"display:flex;flex-wrap:wrap;gap:4px;margin-top:4px\">';\n for(var t=0;t<SA_TOOL_LIST.length;t++){\n var tn = SA_TOOL_LIST[t];\n var checked = saTools.indexOf(tn)!==-1;\n html += '<label class=\"tool-toggle-sm\"><label class=\"toggle-sm\"><input type=\"checkbox\" data-sa-tool=\"'+i+'\" data-tool-name=\"'+tn+'\"'+(checked?' checked':'')+'><span></span></label> '+tn+'</label>';\n }\n html += '</div></div>';\n html += '<div style=\"display:flex;align-items:center;gap:12px;margin-top:8px\"><span style=\"font-size:13px;font-weight:500\">Expand Context</span><label class=\"toggle\"><input type=\"checkbox\" data-sa-expand=\"'+i+'\"'+(sa.expandContext?' checked':'')+'><span></span></label></div>';\n html += '</div></div>';\n }\n container.innerHTML = html;\n // Bind expandContext listeners\n container.querySelectorAll('[data-sa-expand]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saExpand);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].expandContext = cb.checked;\n markDirty();\n renderSubAgentCards();\n }\n });\n });\n // Bind toggle listeners\n container.querySelectorAll('[data-sa-toggle]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saToggle);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n currentConfig.agent.customSubAgents[idx].enabled = cb.checked;\n markDirty();\n }\n });\n });\n // Bind tool toggle listeners\n container.querySelectorAll('[data-sa-tool]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saTool);\n var toolName = cb.dataset.toolName;\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n var tools = currentConfig.agent.customSubAgents[idx].tools || [];\n if(cb.checked){\n if(tools.indexOf(toolName)===-1) tools.push(toolName);\n } else {\n tools = tools.filter(function(t){return t!==toolName;});\n }\n currentConfig.agent.customSubAgents[idx].tools = tools;\n markDirty();\n }\n });\n });\n}\nfunction confirmDeleteSubAgent(idx){\n _saDeleteIdx = idx;\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var name = sas[idx] ? sas[idx].name : 'this subagent';\n document.getElementById('saDeleteName').textContent = name;\n document.getElementById('saDeleteModal').classList.add('open');\n}\nfunction closeSaDeleteModal(){\n document.getElementById('saDeleteModal').classList.remove('open');\n _saDeleteIdx = -1;\n}\nfunction doDeleteSubAgent(){\n if(_saDeleteIdx >= 0) deleteSubAgent(_saDeleteIdx);\n closeSaDeleteModal();\n saveConfig();\n}\nfunction updateSaField(idx, field, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx][field] = value;\n markDirty();\n }\n}\nfunction updateSaTools(idx, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].tools = value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n markDirty();\n }\n}\nfunction showAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = '';\n document.getElementById('newSaName').value = '';\n document.getElementById('newSaDesc').value = '';\n document.getElementById('newSaPrompt').value = '';\n document.getElementById('newSaModel').value = 'inherit';\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){cb.checked = cb.dataset.newSaTool!=='Bash';});\n document.getElementById('newSaExpandContext').checked = false;\n document.getElementById('newSaName').focus();\n}\nfunction hideAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = 'none';\n}\nasync function addSubAgent(){\n var name = document.getElementById('newSaName').value.trim();\n var desc = document.getElementById('newSaDesc').value.trim();\n var prompt = document.getElementById('newSaPrompt').value.trim();\n var model = document.getElementById('newSaModel').value;\n var tools = [];\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){if(cb.checked) tools.push(cb.dataset.newSaTool);});\n if(!name){ toast('Name is required','err'); return; }\n if(!/^[a-zA-Z0-9_\\-\\[\\]!]+$/.test(name)){ toast('Name may only contain a-z A-Z 0-9 - _ [ ] !','err'); return; }\n if(!desc || desc.length < 10){ toast('Description must be at least 10 characters','err'); return; }\n if(!prompt || prompt.length < 10){ toast('Prompt must be at least 10 characters','err'); return; }\n if(!currentConfig){ currentConfig = await fetchAPI('/config'); }\n if(!currentConfig.agent) currentConfig.agent = {};\n if(!currentConfig.agent.customSubAgents) currentConfig.agent.customSubAgents = [];\n var expandContext = document.getElementById('newSaExpandContext').checked;\n currentConfig.agent.customSubAgents.push({ name:name, description:desc, prompt:prompt, model:model, tools:tools, expandContext:expandContext, enabled:false });\n hideAddSubAgent();\n renderSubAgentCards();\n saveConfig();\n}\nfunction deleteSubAgent(idx){\n if(!currentConfig || !currentConfig.agent || !currentConfig.agent.customSubAgents) return;\n currentConfig.agent.customSubAgents.splice(idx, 1);\n renderSubAgentCards();\n markDirty();\n}\n\n/* ---- Vars ---- */\nvar _editVarIdx = -1;\nvar _deleteVarIdx = -1;\n\nfunction showAddVar(){\n document.getElementById('addVarForm').style.display='';\n document.getElementById('newVarName').value='';\n document.getElementById('newVarEnvVar').value='';\n document.getElementById('newVarApiKey').value='';\n}\nfunction hideAddVar(){ document.getElementById('addVarForm').style.display='none'; }\nfunction addVar(){\n var name = document.getElementById('newVarName').value.trim();\n var useEnvVar = document.getElementById('newVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('newVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n if(!currentConfig.models) currentConfig.models=[];\n currentConfig.models.push({id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar});\n hideAddVar();\n renderVarsTable();\n saveConfig();\n}\nasync function loadVars(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderVarsTable();\n}\nfunction renderVarsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('varsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')===-1) continue;\n hasVisible = true;\n var envDisplay = m.useEnvVar||m.id||'';\n tbody.innerHTML += '<tr data-var-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(envDisplay)+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditVar('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteVar('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"3\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No vars in registry</td></tr>';\n }\n document.getElementById('editVarForm').style.display='none';\n _editVarIdx = -1;\n}\nfunction startEditVar(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editVarIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editVarName').value = m.name||'';\n document.getElementById('editVarEnvVar').value = m.useEnvVar||'';\n document.getElementById('editVarApiKey').value = m.apiKey||'';\n document.getElementById('editVarForm').style.display='';\n}\nfunction finishEditVar(){\n if(_editVarIdx<0 || !currentConfig.models||!currentConfig.models[_editVarIdx]) return;\n var name = document.getElementById('editVarName').value.trim();\n var useEnvVar = document.getElementById('editVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('editVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n currentConfig.models[_editVarIdx] = {id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar};\n _editVarIdx = -1;\n renderVarsTable();\n saveConfig();\n}\nfunction cancelEditVar(){\n _editVarIdx = -1;\n document.getElementById('editVarForm').style.display='none';\n}\nfunction confirmDeleteVar(idx){\n _deleteVarIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this var';\n document.getElementById('varDeleteName').textContent = name;\n document.getElementById('varDeleteModal').classList.add('open');\n}\nfunction closeVarDeleteModal(){\n document.getElementById('varDeleteModal').classList.remove('open');\n _deleteVarIdx = -1;\n}\nfunction doDeleteVar(){\n if(_deleteVarIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteVarIdx,1);\n renderVarsTable();\n saveConfig();\n }\n closeVarDeleteModal();\n}\n\n/* ---- Internal Tools discovery ---- */\nvar _internalToolsLoaded = false;\nasync function openInternalToolsModal(){\n document.getElementById('internalToolsModal').classList.add('open');\n if(_internalToolsLoaded) return;\n try {\n var data = await fetchAPI('/internal-tools');\n var wrap = document.getElementById('internalToolsBody');\n if(!data || !data.length){ wrap.innerHTML = '<p style=\"color:var(--text-muted)\">No internal tool servers registered.</p>'; _internalToolsLoaded = true; return; }\n var html = '<table class=\"tbl\" style=\"font-size:13px\"><thead><tr><th style=\"width:18%\">Server</th><th style=\"width:20%\">Tool</th><th>Parameters</th></tr></thead><tbody>';\n for(var s = 0; s < data.length; s++){\n var srv = data[s];\n var toolCount = srv.tools.length || 1;\n for(var t = 0; t < srv.tools.length; t++){\n var tl = srv.tools[t];\n html += '<tr>';\n if(t === 0) html += '<td rowspan=\"'+toolCount+'\" style=\"vertical-align:top\"><strong>'+esc(srv.server)+'</strong></td>';\n html += '<td style=\"vertical-align:top\"><code>'+esc(tl.name)+'</code><div style=\"color:var(--text-muted);font-size:11px;margin-top:4px\">'+esc(tl.description)+'</div></td>';\n if(tl.params.length === 0){\n html += '<td style=\"color:var(--text-muted);font-style:italic\">none</td>';\n } else {\n html += '<td>';\n for(var p = 0; p < tl.params.length; p++){\n var pm = tl.params[p];\n if(p > 0) html += '<br>';\n html += '<code>'+esc(pm.name)+'</code> <span style=\"color:var(--text-muted)\">('+esc(pm.type)+(pm.required?'':',opt')+')</span>';\n if(pm.description) html += ' — <span style=\"font-size:12px\">'+esc(pm.description)+'</span>';\n }\n html += '</td>';\n }\n html += '</tr>';\n }\n if(srv.tools.length === 0){\n html += '<tr><td><strong>'+esc(srv.server)+'</strong></td><td colspan=\"2\" style=\"color:var(--text-muted);font-style:italic\">No tools</td></tr>';\n }\n }\n html += '</tbody></table>';\n wrap.innerHTML = html;\n _internalToolsLoaded = true;\n } catch(err) {\n document.getElementById('internalToolsBody').innerHTML = '<p style=\"color:var(--danger)\">Failed to load: '+esc(String(err))+'</p>';\n }\n}\n"}
1
+ export function agentJS(){return"\nvar SA_TOOL_LIST = ['Read','Write','Edit','Bash','Glob','Grep','WebSearch','WebFetch'];\n\nvar _editModelIdx = -1;\n\n/* ---- Models ---- */\nfunction showAddModel(){\n document.getElementById('addModelForm').style.display='';\n // Reset form\n document.getElementById('newModelId').value='';\n document.getElementById('newModelName').value='';\n document.getElementById('newModelBaseURL').value='https://api.openai.com/v1';\n document.getElementById('newModelApiKey').value='';\n document.getElementById('newModelEnvVar').value='';\n document.getElementById('newModelType').value='external';\n document.getElementById('newModelProxy').value='not-used';\n document.getElementById('newModelFastUrl').value='';\n document.getElementById('newModelFastProxyApiKey').value='';\n document.getElementById('newModelContextWindow').value='200000';\n document.getElementById('newModelCostInput').value='0';\n document.getElementById('newModelCostOutput').value='0';\n document.getElementById('newModelCostCacheRead').value='0';\n document.getElementById('newModelCostCacheWrite').value='0';\n updateNewModelApiFields();\n}\nfunction hideAddModel(){ document.getElementById('addModelForm').style.display='none'; }\nfunction sanitizeEnvVarInput(el){\n var v = el.value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'');\n if(v !== el.value) el.value = v;\n}\nfunction updateNewModelApiFields(){\n var type = document.getElementById('newModelType').value;\n document.getElementById('newModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('newModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('newModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('newModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateNewModelProxyFields();\n}\nfunction updateNewModelProxyFields(){\n var proxy = document.getElementById('newModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('newModelFastUrl');\n var fk = document.getElementById('newModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nfunction updateEditModelApiFields(){\n var type = document.getElementById('editModelType').value;\n document.getElementById('editModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('editModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('editModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n var extraFields = document.getElementById('editModelExtraFields');\n if(extraFields) extraFields.style.display = type==='external' ? '' : 'none';\n updateEditModelProxyFields();\n}\nfunction updateEditModelProxyFields(){\n var proxy = document.getElementById('editModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('editModelFastUrl');\n var fk = document.getElementById('editModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nasync function loadModels(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderModelsTable();\n}\nfunction renderModelsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('modelsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')!==-1) continue;\n hasVisible = true;\n var typeBadges = types.map(function(t){ return '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px;margin-right:2px\">'+esc(t)+'</span>'; }).join('');\n var proxyBadge = (m.proxy && m.proxy !== 'not-used') ? ' <span class=\"badge badge-green\" style=\"font-size:11px;padding:1px 6px\">'+esc(m.proxy)+'</span>' : '';\n tbody.innerHTML += '<tr data-model-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(m.id)+'</td><td>'+typeBadges+proxyBadge+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditModel('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteModel('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"4\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No models in registry</td></tr>';\n }\n // Hide edit form when re-rendering\n document.getElementById('editModelForm').style.display='none';\n _editModelIdx = -1;\n}\nfunction addModel(){\n var id = document.getElementById('newModelId').value.trim();\n var name = document.getElementById('newModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('newModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('newModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('newModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('newModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('newModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('newModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('newModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('newModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('newModelCostCacheWrite').value)||0) : 0;\n if(!currentConfig.models) currentConfig.models=[];\n var dup = currentConfig.models.some(function(m){ return m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models.push({id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite});\n hideAddModel();\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nvar _deleteModelIdx = -1;\nfunction confirmDeleteModel(idx){\n _deleteModelIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this model';\n document.getElementById('modelDeleteName').textContent = name;\n document.getElementById('modelDeleteModal').classList.add('open');\n}\nfunction closeModelDeleteModal(){\n document.getElementById('modelDeleteModal').classList.remove('open');\n _deleteModelIdx = -1;\n}\nfunction doDeleteModel(){\n if(_deleteModelIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteModelIdx,1);\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n }\n closeModelDeleteModal();\n}\nfunction startEditModel(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editModelIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editModelId').value = m.id||'';\n document.getElementById('editModelName').value = m.name||'';\n document.getElementById('editModelBaseURL').value = m.baseURL||'';\n document.getElementById('editModelApiKey').value = m.apiKey||'';\n document.getElementById('editModelEnvVar').value = m.useEnvVar||'';\n document.getElementById('editModelProxy').value = m.proxy||'not-used';\n document.getElementById('editModelFastUrl').value = m.fastUrl||'';\n document.getElementById('editModelFastProxyApiKey').value = m.fastProxyApiKey||'';\n document.getElementById('editModelContextWindow').value = m.contextWindow||200000;\n document.getElementById('editModelCostInput').value = m.costInput||0;\n document.getElementById('editModelCostOutput').value = m.costOutput||0;\n document.getElementById('editModelCostCacheRead').value = m.costCacheRead||0;\n document.getElementById('editModelCostCacheWrite').value = m.costCacheWrite||0;\n var types = m.types||['external'];\n document.getElementById('editModelType').value = types[0]||'external';\n updateEditModelApiFields();\n // Position edit form after the table\n document.getElementById('editModelForm').style.display='';\n}\nfunction finishEditModel(){\n if(_editModelIdx<0 || !currentConfig.models||!currentConfig.models[_editModelIdx]) return;\n var id = document.getElementById('editModelId').value.trim();\n var name = document.getElementById('editModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('editModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('editModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('editModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('editModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('editModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastProxyApiKey').value.trim() : '';\n var contextWindow = type==='external' ? (parseInt(document.getElementById('editModelContextWindow').value)||200000) : 200000;\n var costInput = type==='external' ? (parseFloat(document.getElementById('editModelCostInput').value)||0) : 0;\n var costOutput = type==='external' ? (parseFloat(document.getElementById('editModelCostOutput').value)||0) : 0;\n var costCacheRead = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheRead').value)||0) : 0;\n var costCacheWrite = type==='external' ? (parseFloat(document.getElementById('editModelCostCacheWrite').value)||0) : 0;\n var dup = currentConfig.models.some(function(m, i){ return i!==_editModelIdx && m.name===name && m.id===id; });\n if(dup){ toast('A model configuration with the same Model Name and Model ID already exists','err'); return; }\n currentConfig.models[_editModelIdx] = {id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar, contextWindow:contextWindow, costInput:costInput, costOutput:costOutput, costCacheRead:costCacheRead, costCacheWrite:costCacheWrite};\n _editModelIdx = -1;\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nfunction cancelEditModel(){\n _editModelIdx = -1;\n document.getElementById('editModelForm').style.display='none';\n}\nfunction populateModelSelects(){\n var models = (currentConfig&&currentConfig.models)||[];\n var picoEnabled = document.getElementById('picoEnabled') && document.getElementById('picoEnabled').checked;\n var picoNames = {};\n if (picoEnabled) { for (var pi=0; pi<_picoModels.length; pi++) picoNames[_picoModels[pi].name] = true; }\n var internalModels = models.filter(function(m){ var t=m.types||['external']; return t.indexOf('internal')!==-1 || (t.indexOf('external')!==-1 && m.proxy && m.proxy!=='not-used') || (picoEnabled && t.indexOf('external')!==-1 && picoNames[m.name]); });\n var selects = {\n agentModel: {val:'', allowNone:false},\n agentMainFallback: {val:'', allowNone:true}\n };\n for(var key in selects){\n var el = document.getElementById(key);\n if(!el) continue;\n selects[key].val = el.value;\n el.innerHTML='';\n if(selects[key].allowNone) el.innerHTML += '<option value=\"\">None</option>';\n for(var i=0;i<internalModels.length;i++){\n el.innerHTML += '<option value=\"'+esc(internalModels[i].name+':'+internalModels[i].id)+'\">'+esc(internalModels[i].name)+' ('+esc(internalModels[i].id)+')</option>';\n }\n el.value = selects[key].val;\n }\n populateSTTModelSelect();\n populateMemSearchModelSelect();\n populatePicoModelSelect();\n populateRollingModelSelect();\n}\nfunction populateSTTModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('sttModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\nfunction populateMemSearchModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('memSearchModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name+':'+models[i].id)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\n\n/* ---- Pico Agent ---- */\nvar _picoModels = []; // [{name, piProvider, piModelId, contextWindow}]\nvar _agentLoading = false; // suppress markDirty during initial load\n\nfunction detectPiProvider(model) {\n var url = (model.baseURL || '').toLowerCase();\n if (url.includes('openrouter.ai')) return 'openrouter';\n if (url.includes('openai.com')) return 'openai';\n if (url.includes('x.ai')) return 'xai';\n if (url.includes('googleapis.com')) return 'google';\n if (url.includes('groq.com')) return 'groq';\n if (url.includes('mistral.ai')) return 'mistral';\n return 'openai';\n}\n\nfunction loadPicoAgent() {\n var a = (currentConfig && currentConfig.agent) || {};\n var pico = a.picoAgent || {};\n // Fallback: migrate from engine if picoAgent absent and engine.type === \"pi\"\n if (!a.picoAgent && a.engine && a.engine.type === 'pi') {\n pico = { enabled: true, modelRefs: [], rollingMemoryModel: '' };\n if (a.engine.piModelRef) {\n pico.modelRefs = [a.engine.piModelRef];\n }\n }\n var el = document.getElementById('picoEnabled');\n if (el) el.checked = !!pico.enabled;\n // Parse modelRefs into _picoModels\n _picoModels = [];\n var refs = pico.modelRefs || [];\n var models = (currentConfig && currentConfig.models) || [];\n for (var i = 0; i < refs.length; i++) {\n var parts = refs[i].split(':');\n var name = parts[0] || '';\n var piProvider = parts.length >= 3 ? parts[1] : '';\n var piModelId = parts.length >= 3 ? parts.slice(2).join(':') : (parts[1] || '');\n // Find matching model entry for contextWindow\n var matched = models.filter(function(m){ return m.name === name; })[0];\n var ctx = (matched && matched.contextWindow) || 200000;\n if (!piProvider && matched) piProvider = detectPiProvider(matched);\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n }\n updatePicoFields();\n renderPicoModelList();\n // Set rolling model AFTER populateRollingModelSelect() has built the options\n var rollingEl = document.getElementById('picoRollingModel');\n if (rollingEl) rollingEl.value = pico.rollingMemoryModel || '';\n}\n\nfunction updatePicoFields() {\n var enabled = document.getElementById('picoEnabled').checked;\n var fields = document.getElementById('picoFields');\n if (fields) fields.style.display = enabled ? '' : 'none';\n if (enabled) {\n populatePicoModelSelect();\n populateRollingModelSelect();\n }\n populateModelSelects();\n if (!_agentLoading) markDirty();\n}\n\nfunction populatePicoModelSelect() {\n var models = (currentConfig && currentConfig.models) || [];\n var el = document.getElementById('picoModelSelect');\n if (!el) return;\n el.innerHTML = '<option value=\"\" disabled selected>Select a model...</option>';\n var alreadyAdded = {};\n for (var j = 0; j < _picoModels.length; j++) alreadyAdded[_picoModels[j].name] = true;\n var hasOptions = false;\n for (var i = 0; i < models.length; i++) {\n var types = models[i].types || ['external'];\n if (types.indexOf('external') === -1) continue;\n if (alreadyAdded[models[i].name]) continue;\n el.innerHTML += '<option value=\"' + esc(models[i].name) + '\">' + esc(models[i].name) + ' (' + esc(models[i].id) + ')</option>';\n hasOptions = true;\n }\n if (!hasOptions) {\n el.innerHTML = '<option value=\"\" disabled selected>-- no available models --</option>';\n }\n}\n\nfunction populateRollingModelSelect() {\n var el = document.getElementById('picoRollingModel');\n if (!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- none --</option>';\n // Populate from _picoModels so values are full modelRefs (Name:provider:modelId)\n for (var i = 0; i < _picoModels.length; i++) {\n var m = _picoModels[i];\n var ref = m.name + ':' + m.piProvider + ':' + m.piModelId;\n el.innerHTML += '<option value=\"' + esc(ref) + '\">' + esc(m.name) + ' (' + esc(m.piProvider + ':' + m.piModelId) + ')</option>';\n }\n el.value = prev;\n}\n\nfunction addPicoModel() {\n var sel = document.getElementById('picoModelSelect');\n var name = sel.value;\n if (!name) return;\n var models = (currentConfig && currentConfig.models) || [];\n var matched = models.filter(function(m){ return m.name === name; })[0];\n if (!matched) return;\n var piProvider = detectPiProvider(matched);\n var piModelId = matched.id || '';\n // Strip registry namespace prefix (e.g. \"openrouter:openai/gpt-5.2\" → \"openai/gpt-5.2\")\n var nsIdx = piModelId.indexOf(':');\n if (nsIdx > 0) piModelId = piModelId.substring(nsIdx + 1);\n var ctx = matched.contextWindow || 200000;\n _picoModels.push({ name: name, piProvider: piProvider, piModelId: piModelId, contextWindow: ctx });\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction removePicoModel(idx) {\n _picoModels.splice(idx, 1);\n renderPicoModelList();\n populatePicoModelSelect();\n populateModelSelects();\n markDirty();\n}\n\nfunction clearRollingModel() {\n var el = document.getElementById('picoRollingModel');\n if (el) el.value = '';\n markDirty();\n}\n\nfunction renderPicoModelList() {\n var container = document.getElementById('picoModelList');\n if (!container) return;\n if (_picoModels.length === 0) {\n container.innerHTML = '<div style=\"color:var(--text-muted);font-size:13px;padding:8px 0\">No models added. Use the select above to add models.</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < _picoModels.length; i++) {\n var m = _picoModels[i];\n var ctxLabel = m.contextWindow >= 1000 ? Math.round(m.contextWindow / 1000) + 'k ctx' : m.contextWindow + ' ctx';\n html += '<div class=\"pico-model-item\" draggable=\"true\" data-pico-idx=\"' + i + '\">';\n html += '<span class=\"pico-drag-handle\">⠇</span>';\n html += '<span class=\"pico-model-name\">' + esc(m.name) + '</span>';\n html += '<span class=\"pico-model-ctx\">' + esc(ctxLabel) + '</span>';\n if (i === 0) html += '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px\">default</span>';\n html += '<button class=\"pico-model-remove\" onclick=\"removePicoModel(' + i + ')\">&times;</button>';\n html += '</div>';\n }\n container.innerHTML = html;\n // Attach drag events\n var items = container.querySelectorAll('.pico-model-item');\n items.forEach(function(item) {\n item.addEventListener('dragstart', function(e) {\n e.dataTransfer.setData('text/plain', item.dataset.picoIdx);\n item.classList.add('dragging');\n });\n item.addEventListener('dragend', function() {\n item.classList.remove('dragging');\n });\n item.addEventListener('dragover', function(e) {\n e.preventDefault();\n });\n item.addEventListener('drop', function(e) {\n e.preventDefault();\n var fromIdx = parseInt(e.dataTransfer.getData('text/plain'));\n var toIdx = parseInt(item.dataset.picoIdx);\n if (fromIdx === toIdx) return;\n var moved = _picoModels.splice(fromIdx, 1)[0];\n _picoModels.splice(toIdx, 0, moved);\n renderPicoModelList();\n markDirty();\n });\n });\n}\n\n/* ---- Model ref upgrade ---- */\nfunction upgradeModelRef(val, models){\n if(!val) return val;\n if(val.indexOf(':')!==-1) return val;\n var m = models.filter(function(x){ return x.name===val; })[0];\n return m ? m.name+':'+m.id : val;\n}\n\n/* ---- Agent ---- */\nasync function loadAgent(){\n currentConfig = await fetchAPI('/config');\n const a = currentConfig.agent||{};\n var models = currentConfig.models||[];\n // Load pico agent FIRST so _picoModels is populated before populateModelSelects()\n _agentLoading = true;\n loadPicoAgent();\n populateModelSelects();\n document.getElementById('agentModel').value = upgradeModelRef(a.model||'', models);\n document.getElementById('agentMainFallback').value = upgradeModelRef(a.mainFallback||'', models);\n document.getElementById('agentMaxTurns').value = a.maxTurns||10;\n document.getElementById('agentPermMode').value = a.permissionMode||'default';\n document.getElementById('agentSessionTTL').value = a.sessionTTL||3600;\n document.getElementById('agentSettingSources').value = a.settingSources||'project';\n document.getElementById('agentCoderSkill').checked = !!a.builtinCoderSkill;\n document.getElementById('agentAutoRenew').value = a.autoRenew||0;\n var allowed = a.allowedTools||[];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){cb.checked = allowed.indexOf(cb.dataset.tool)!==-1;});\n document.getElementById('agentQueueMode').value = a.queueMode||'collect';\n document.getElementById('agentDebounceMs').value = a.queueDebounceMs!=null ? a.queueDebounceMs : 1500;\n document.getElementById('agentQueueCap').value = a.queueCap!=null ? a.queueCap : 20;\n document.getElementById('agentDropPolicy').value = a.queueDropPolicy||'summarize';\n document.getElementById('agentInflightTyping').checked = a.inflightTyping!==false;\n document.getElementById('agentAutoApprove').checked = a.autoApproveTools!==false;\n updateQueueFields();\n _agentLoading = false;\n sectionsLoaded.agent = true;\n}\n\n/* ---- Queue fields visibility ---- */\nfunction updateQueueFields(){\n var mode = document.getElementById('agentQueueMode').value;\n document.getElementById('queueCollectFields').style.display = mode==='collect' ? '' : 'none';\n}\n\n/* ---- SubAgents ---- */\nvar _saDeleteIdx = -1;\nasync function loadSubAgents(){\n currentConfig = await fetchAPI('/config');\n renderSubAgentCards();\n}\nfunction toggleSaAccordion(idx){\n var el = document.querySelector('.sa-acc[data-sa-idx=\"'+idx+'\"]');\n if(el) el.classList.toggle('open');\n}\nfunction renderSubAgentCards(){\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var container = document.getElementById('subAgentCards');\n var empty = document.getElementById('subAgentEmpty');\n if(sas.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n return;\n }\n empty.style.display = 'none';\n var order = [];\n for(var k=0;k<sas.length;k++) order.push(k);\n order.sort(function(a,b){ return (sas[a].name||'').localeCompare(sas[b].name||''); });\n var html = '';\n for(var oi = 0; oi < order.length; oi++){\n var i = order[oi];\n var sa = sas[i];\n html += '<div class=\"sa-acc\" data-sa-idx=\"'+i+'\">';\n html += '<div class=\"sa-acc-header\" onclick=\"toggleSaAccordion('+i+')\">';\n html += '<svg class=\"sa-acc-chevron\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>';\n html += '<span class=\"sa-acc-name\">' + esc(sa.name) + '</span>';\n if(sa.expandContext) html += '<span class=\"badge badge-blue\" style=\"font-size:10px;padding:1px 6px\">expanded</span>';\n html += '<label class=\"toggle\" onclick=\"event.stopPropagation()\"><input type=\"checkbox\" data-sa-toggle=\"'+i+'\" '+(sa.enabled?'checked':'')+'><span></span></label>';\n html += '<button class=\"btn-danger btn-sm\" onclick=\"event.stopPropagation();confirmDeleteSubAgent('+i+')\">Delete</button>';\n html += '</div>';\n html += '<div class=\"sa-acc-body\">';\n html += '<div class=\"field\"><label>Description</label><textarea data-sa-field=\"'+i+'.description\" rows=\"2\" oninput=\"updateSaField('+i+',&quot;description&quot;,this.value)\">'+esc(sa.description)+'</textarea></div>';\n html += '<div class=\"field\"><label>Prompt</label><textarea data-sa-field=\"'+i+'.prompt\" rows=\"3\" oninput=\"updateSaField('+i+',&quot;prompt&quot;,this.value)\">'+esc(sa.prompt)+'</textarea></div>';\n html += '<div class=\"field\"><label>Model</label><select data-sa-field=\"'+i+'.model\" onchange=\"updateSaField('+i+',&quot;model&quot;,this.value)\">';\n var saModels = ['inherit','sonnet','opus','haiku'];\n for(var j=0;j<saModels.length;j++){\n html += '<option value=\"'+saModels[j]+'\"'+(sa.model===saModels[j]?' selected':'')+'>'+saModels[j]+'</option>';\n }\n html += '</select></div>';\n var saTools = sa.tools||[];\n html += '<div class=\"field\"><label>Tools</label><div style=\"display:flex;flex-wrap:wrap;gap:4px;margin-top:4px\">';\n for(var t=0;t<SA_TOOL_LIST.length;t++){\n var tn = SA_TOOL_LIST[t];\n var checked = saTools.indexOf(tn)!==-1;\n html += '<label class=\"tool-toggle-sm\"><label class=\"toggle-sm\"><input type=\"checkbox\" data-sa-tool=\"'+i+'\" data-tool-name=\"'+tn+'\"'+(checked?' checked':'')+'><span></span></label> '+tn+'</label>';\n }\n html += '</div></div>';\n html += '<div style=\"display:flex;align-items:center;gap:12px;margin-top:8px\"><span style=\"font-size:13px;font-weight:500\">Expand Context</span><label class=\"toggle\"><input type=\"checkbox\" data-sa-expand=\"'+i+'\"'+(sa.expandContext?' checked':'')+'><span></span></label></div>';\n html += '</div></div>';\n }\n container.innerHTML = html;\n // Bind expandContext listeners\n container.querySelectorAll('[data-sa-expand]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saExpand);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].expandContext = cb.checked;\n markDirty();\n renderSubAgentCards();\n }\n });\n });\n // Bind toggle listeners\n container.querySelectorAll('[data-sa-toggle]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saToggle);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n currentConfig.agent.customSubAgents[idx].enabled = cb.checked;\n markDirty();\n }\n });\n });\n // Bind tool toggle listeners\n container.querySelectorAll('[data-sa-tool]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saTool);\n var toolName = cb.dataset.toolName;\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n var tools = currentConfig.agent.customSubAgents[idx].tools || [];\n if(cb.checked){\n if(tools.indexOf(toolName)===-1) tools.push(toolName);\n } else {\n tools = tools.filter(function(t){return t!==toolName;});\n }\n currentConfig.agent.customSubAgents[idx].tools = tools;\n markDirty();\n }\n });\n });\n}\nfunction confirmDeleteSubAgent(idx){\n _saDeleteIdx = idx;\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var name = sas[idx] ? sas[idx].name : 'this subagent';\n document.getElementById('saDeleteName').textContent = name;\n document.getElementById('saDeleteModal').classList.add('open');\n}\nfunction closeSaDeleteModal(){\n document.getElementById('saDeleteModal').classList.remove('open');\n _saDeleteIdx = -1;\n}\nfunction doDeleteSubAgent(){\n if(_saDeleteIdx >= 0) deleteSubAgent(_saDeleteIdx);\n closeSaDeleteModal();\n saveConfig();\n}\nfunction updateSaField(idx, field, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx][field] = value;\n markDirty();\n }\n}\nfunction updateSaTools(idx, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].tools = value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n markDirty();\n }\n}\nfunction showAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = '';\n document.getElementById('newSaName').value = '';\n document.getElementById('newSaDesc').value = '';\n document.getElementById('newSaPrompt').value = '';\n document.getElementById('newSaModel').value = 'inherit';\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){cb.checked = cb.dataset.newSaTool!=='Bash';});\n document.getElementById('newSaExpandContext').checked = false;\n document.getElementById('newSaName').focus();\n}\nfunction hideAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = 'none';\n}\nasync function addSubAgent(){\n var name = document.getElementById('newSaName').value.trim();\n var desc = document.getElementById('newSaDesc').value.trim();\n var prompt = document.getElementById('newSaPrompt').value.trim();\n var model = document.getElementById('newSaModel').value;\n var tools = [];\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){if(cb.checked) tools.push(cb.dataset.newSaTool);});\n if(!name){ toast('Name is required','err'); return; }\n if(!/^[a-zA-Z0-9_\\-\\[\\]!]+$/.test(name)){ toast('Name may only contain a-z A-Z 0-9 - _ [ ] !','err'); return; }\n if(!desc || desc.length < 10){ toast('Description must be at least 10 characters','err'); return; }\n if(!prompt || prompt.length < 10){ toast('Prompt must be at least 10 characters','err'); return; }\n if(!currentConfig){ currentConfig = await fetchAPI('/config'); }\n if(!currentConfig.agent) currentConfig.agent = {};\n if(!currentConfig.agent.customSubAgents) currentConfig.agent.customSubAgents = [];\n var expandContext = document.getElementById('newSaExpandContext').checked;\n currentConfig.agent.customSubAgents.push({ name:name, description:desc, prompt:prompt, model:model, tools:tools, expandContext:expandContext, enabled:false });\n hideAddSubAgent();\n renderSubAgentCards();\n saveConfig();\n}\nfunction deleteSubAgent(idx){\n if(!currentConfig || !currentConfig.agent || !currentConfig.agent.customSubAgents) return;\n currentConfig.agent.customSubAgents.splice(idx, 1);\n renderSubAgentCards();\n markDirty();\n}\n\n/* ---- Vars ---- */\nvar _editVarIdx = -1;\nvar _deleteVarIdx = -1;\n\nfunction showAddVar(){\n document.getElementById('addVarForm').style.display='';\n document.getElementById('newVarName').value='';\n document.getElementById('newVarEnvVar').value='';\n document.getElementById('newVarApiKey').value='';\n}\nfunction hideAddVar(){ document.getElementById('addVarForm').style.display='none'; }\nfunction addVar(){\n var name = document.getElementById('newVarName').value.trim();\n var useEnvVar = document.getElementById('newVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('newVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n if(!currentConfig.models) currentConfig.models=[];\n currentConfig.models.push({id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar});\n hideAddVar();\n renderVarsTable();\n saveConfig();\n}\nasync function loadVars(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderVarsTable();\n}\nfunction renderVarsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('varsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')===-1) continue;\n hasVisible = true;\n var envDisplay = m.useEnvVar||m.id||'';\n tbody.innerHTML += '<tr data-var-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(envDisplay)+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditVar('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteVar('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"3\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No vars in registry</td></tr>';\n }\n document.getElementById('editVarForm').style.display='none';\n _editVarIdx = -1;\n}\nfunction startEditVar(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editVarIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editVarName').value = m.name||'';\n document.getElementById('editVarEnvVar').value = m.useEnvVar||'';\n document.getElementById('editVarApiKey').value = m.apiKey||'';\n document.getElementById('editVarForm').style.display='';\n}\nfunction finishEditVar(){\n if(_editVarIdx<0 || !currentConfig.models||!currentConfig.models[_editVarIdx]) return;\n var name = document.getElementById('editVarName').value.trim();\n var useEnvVar = document.getElementById('editVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('editVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n currentConfig.models[_editVarIdx] = {id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar};\n _editVarIdx = -1;\n renderVarsTable();\n saveConfig();\n}\nfunction cancelEditVar(){\n _editVarIdx = -1;\n document.getElementById('editVarForm').style.display='none';\n}\nfunction confirmDeleteVar(idx){\n _deleteVarIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this var';\n document.getElementById('varDeleteName').textContent = name;\n document.getElementById('varDeleteModal').classList.add('open');\n}\nfunction closeVarDeleteModal(){\n document.getElementById('varDeleteModal').classList.remove('open');\n _deleteVarIdx = -1;\n}\nfunction doDeleteVar(){\n if(_deleteVarIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteVarIdx,1);\n renderVarsTable();\n saveConfig();\n }\n closeVarDeleteModal();\n}\n\n/* ---- Internal Tools discovery ---- */\nvar _internalToolsLoaded = false;\nasync function openInternalToolsModal(){\n document.getElementById('internalToolsModal').classList.add('open');\n if(_internalToolsLoaded) return;\n try {\n var data = await fetchAPI('/internal-tools');\n var wrap = document.getElementById('internalToolsBody');\n if(!data || !data.length){ wrap.innerHTML = '<p style=\"color:var(--text-muted)\">No internal tool servers registered.</p>'; _internalToolsLoaded = true; return; }\n var html = '<table class=\"tbl\" style=\"font-size:13px\"><thead><tr><th style=\"width:18%\">Server</th><th style=\"width:20%\">Tool</th><th>Parameters</th></tr></thead><tbody>';\n for(var s = 0; s < data.length; s++){\n var srv = data[s];\n var toolCount = srv.tools.length || 1;\n for(var t = 0; t < srv.tools.length; t++){\n var tl = srv.tools[t];\n html += '<tr>';\n if(t === 0) html += '<td rowspan=\"'+toolCount+'\" style=\"vertical-align:top\"><strong>'+esc(srv.server)+'</strong></td>';\n html += '<td style=\"vertical-align:top\"><code>'+esc(tl.name)+'</code><div style=\"color:var(--text-muted);font-size:11px;margin-top:4px\">'+esc(tl.description)+'</div></td>';\n if(tl.params.length === 0){\n html += '<td style=\"color:var(--text-muted);font-style:italic\">none</td>';\n } else {\n html += '<td>';\n for(var p = 0; p < tl.params.length; p++){\n var pm = tl.params[p];\n if(p > 0) html += '<br>';\n html += '<code>'+esc(pm.name)+'</code> <span style=\"color:var(--text-muted)\">('+esc(pm.type)+(pm.required?'':',opt')+')</span>';\n if(pm.description) html += ' — <span style=\"font-size:12px\">'+esc(pm.description)+'</span>';\n }\n html += '</td>';\n }\n html += '</tr>';\n }\n if(srv.tools.length === 0){\n html += '<tr><td><strong>'+esc(srv.server)+'</strong></td><td colspan=\"2\" style=\"color:var(--text-muted);font-style:italic\">No tools</td></tr>';\n }\n }\n html += '</tbody></table>';\n wrap.innerHTML = html;\n _internalToolsLoaded = true;\n } catch(err) {\n document.getElementById('internalToolsBody').innerHTML = '<p style=\"color:var(--danger)\">Failed to load: '+esc(String(err))+'</p>';\n }\n}\n"}
@@ -1 +1 @@
1
- import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as d}from"./pi-context-compactor.js";import{randomUUID as u}from"node:crypto";let m=null;async function p(){if(!m)try{const e=await import("@mariozechner/pi-ai");m={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return m}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(m,h,k){const{prompt:T,options:b}=m;let _=new AbortController,v=!1,P=!1,$=h.modelId,C=h.provider;const M=b.resume||u();let R=g.get(M);if(!R){let e="";if("string"==typeof b.systemPrompt)e=b.systemPrompt;else if(b.systemPrompt&&"object"==typeof b.systemPrompt){const t=b.systemPrompt.preset,o=b.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}R={id:M,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(M,R)}R.lastAccessTime=Date.now();const I=k??new s;if(k||(I.registerTools(n),b.mcpServers&&Object.keys(b.mcpServers).length>0&&a(b).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&I.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),b.canUseTool&&I.setPermissionChecker(b.canUseTool),!k){const e=b.mcpServers??{};I.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,b.cwd,M);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*W(s){if(P)return;let n=R.systemPrompt;const r=i(b.cwd||"");if(r&&(n+="\n\n"+r),R.messages.length>0){const e=c(R.messages,n,{...d,contextWindowTokens:h.contextWindowTokens??128e3},R.lastCompactedIndex,R.rollingContext);if(e.phase>0&&(R.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${R.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await p();if(t){const o={...d,contextWindowTokens:h.contextWindowTokens??128e3};let s=C,n=$;if(o.summarizationModel){const e=o.summarizationModel.split("/",2);2===e.length?(s=e[0],n=e[1]):n=e[0]}const r=t.getModel(s,n),a=t.stream(r,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of a);const i=await a.result(),c=i.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");c&&(R.rollingContext=c,l(R.messages,c,d.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${R.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}R.lastCompactedIndex=R.messages.length}const a=e(s);R.messages.push(a),R.lastAccessTime=Date.now();const u=b.maxTurns??25;let m=0,g="",k="end_turn",T={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const M=Date.now();for(;m<u&&!P&&!v;){m++;const e=I.getFilteredTools(b.allowedTools,b.disallowedTools);let s,r;try{s=await p()}catch(e){return void(yield y(R,"error_during_execution",g,k,$,M,T,[`${e}`]))}try{r=s.getModel(C,$)}catch(e){return void(yield y(R,"error_during_execution",g,k,$,M,T,[`Model not found: ${C}/${$}. ${e}`]))}const a={systemPrompt:n,messages:R.messages.map(x),tools:e.length>0?e:void 0},i={signal:_.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if(P||v||_.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||_.signal.aborted?(v=!1,void(yield y(R,"error_during_execution",g,k,$,M,T,["aborted"]))):void(yield y(R,"error_during_execution",g,k,$,M,T,[`${e}`]))}w(T,c.usage),k=f(c.stopReason);const l=t(c);yield l,R.messages.push(c);const d=o(l);if(d&&(g=d),"toolUse"!==c.stopReason)break;const u=c.content.filter(e=>"toolCall"===e.type);if(0===u.length)break;for(const e of u){if(P||v||_.signal.aborted)break;const t=await I.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};R.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await I.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};R.messages.push(a)}if(P||v||_.signal.aborted)return v=!1,void(yield y(R,"error_during_execution",g,k,$,M,T,["aborted"]))}m>=u&&!P?yield y(R,"error_max_turns",g,k,$,M,T):(v=!1,yield y(R,"success",g,k,$,M,T))}const D=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:M};try{for await(const e of T){if(P)break;(v||_.signal.aborted)&&(v=!1,_=new AbortController);for await(const t of W(e)){if(P)break;yield t}}}catch(e){P||(yield{type:"result",subtype:"error_during_execution",session_id:M,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>D,async interrupt(){v=!0,_.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,$=o}else $=e},close(){P=!0,_.abort(),g.delete(M)},async supportedModels(){try{const e=await p();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
1
+ import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as d}from"./pi-context-compactor.js";import{randomUUID as u}from"node:crypto";let m=null;async function p(){if(!m)try{const e=await import("@mariozechner/pi-ai");m={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return m}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(m,h,b){const{prompt:_,options:k}=m;let T=new AbortController,v=!1,P=!1,$=h.modelId,C=h.provider;const I=k.resume||u();let R=g.get(I);if(!R){let e="";if("string"==typeof k.systemPrompt)e=k.systemPrompt;else if(k.systemPrompt&&"object"==typeof k.systemPrompt){const t=k.systemPrompt.preset,o=k.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}R={id:I,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(I,R)}R.lastAccessTime=Date.now();const M=b??new s;if(b||(M.registerTools(n),k.mcpServers&&Object.keys(k.mcpServers).length>0&&a(k).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&M.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),k.canUseTool&&M.setPermissionChecker(k.canUseTool),!b){const e=k.mcpServers??{};M.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,k.cwd,I);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*D(s){if(P)return;let n=R.systemPrompt;const r=i(k.cwd||"");if(r&&(n+="\n\n"+r),R.messages.length>0){const e=c(R.messages,n,{...d,contextWindowTokens:h.contextWindowTokens??128e3},R.lastCompactedIndex,R.rollingContext);if(e.phase>0&&(R.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${R.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await p();if(t){const o=h.summarizationProvider||C,s=h.summarizationModelId||$,n=t.getModel(o,s),r=t.stream(n,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of r);const a=await r.result(),i=a.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");i&&(R.rollingContext=i,l(R.messages,i,d.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${R.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}R.lastCompactedIndex=R.messages.length}const a=e(s);R.messages.push(a),R.lastAccessTime=Date.now();const u=k.maxTurns??25;let m=0,g="",b="end_turn",_={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const I=Date.now();for(;m<u&&!P&&!v;){m++;const e=M.getFilteredTools(k.allowedTools,k.disallowedTools);let s,r;try{s=await p()}catch(e){return void(yield y(R,"error_during_execution",g,b,$,I,_,[`${e}`]))}try{r=s.getModel(C,$)}catch(e){return void(yield y(R,"error_during_execution",g,b,$,I,_,[`Model not found: ${C}/${$}. ${e}`]))}const a={systemPrompt:n,messages:R.messages.map(x),tools:e.length>0?e:void 0},i={signal:T.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if(P||v||T.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||T.signal.aborted?(v=!1,void(yield y(R,"error_during_execution",g,b,$,I,_,["aborted"]))):void(yield y(R,"error_during_execution",g,b,$,I,_,[`${e}`]))}w(_,c.usage),b=f(c.stopReason);const l=t(c);yield l,R.messages.push(c);const d=o(l);if(d&&(g=d),"toolUse"!==c.stopReason)break;const u=c.content.filter(e=>"toolCall"===e.type);if(0===u.length)break;for(const e of u){if(P||v||T.signal.aborted)break;const t=await M.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};R.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await M.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};R.messages.push(a)}if(P||v||T.signal.aborted)return v=!1,void(yield y(R,"error_during_execution",g,b,$,I,_,["aborted"]))}m>=u&&!P?yield y(R,"error_max_turns",g,b,$,I,_):(v=!1,yield y(R,"success",g,b,$,I,_))}const W=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:I};try{for await(const e of _){if(P)break;(v||T.signal.aborted)&&(v=!1,T=new AbortController);for await(const t of D(e)){if(P)break;yield t}}}catch(e){P||(yield{type:"result",subtype:"error_during_execution",session_id:I,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>W,async interrupt(){v=!0,T.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,$=o}else $=e},close(){P=!0,T.abort(),g.delete(I)},async supportedModels(){try{const e=await p();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
@@ -168,6 +168,10 @@ export interface PiProviderConfig {
168
168
  costCacheRead?: number;
169
169
  /** Cost per 1M cache write tokens (USD) */
170
170
  costCacheWrite?: number;
171
+ /** Optional provider for Phase 3 summarization. Falls back to session provider if unset. */
172
+ summarizationProvider?: string;
173
+ /** Optional model ID for Phase 3 summarization. Falls back to session model if unset. */
174
+ summarizationModelId?: string;
171
175
  }
172
176
  export interface ModelUsageEntry {
173
177
  inputTokens: number;
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.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 u}from"./agent/session-manager.js";import{buildPrompt as p,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as y}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 k,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as D}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 N}from"./commands/subagents.js";import{CustomSubAgentsCommand as P}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as F}from"./commands/usage.js";import{CronService as O}from"./cron/cron-service.js";import{stripHeartbeatToken as H,isHeartbeatContentEffectivelyEmpty as B}from"./cron/heartbeat-token.js";import{createServerToolsServer as L}from"./tools/server-tools.js";import{createCronToolsServer as Q}from"./tools/cron-tools.js";import{createTTSToolsServer as W}from"./tools/tts-tools.js";import{createMemoryToolsServer as z}from"./tools/memory-tools.js";import{createBrowserToolsServer as G}from"./tools/browser-tools.js";import{createPicoToolsServer as q}from"./tools/pico-tools.js";import{BrowserService as V}from"./browser/browser-service.js";import{MemorySearch as J}from"./memory/memory-search.js";import{stripMediaLines as X}from"./utils/media-response.js";import{loadConfig as Y,loadRawConfig as Z,backupConfig as ee,resolveModelEntry as te,modelRefName as se}from"./config.js";import{stringify as ne}from"yaml";import{createLogger as oe}from"./utils/logger.js";import{SessionErrorHandler as ie}from"./agent/session-error-handler.js";const re=oe("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=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 u(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),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const i=this.createMemorySearch(e);this.browserService=new V;const g=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?Q(this.cronService,()=>this.config):void 0,m=e.tts.enabled?W(()=>this.config):void 0,d=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(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;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,m,i,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g,d),S(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 O({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e)})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=te(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new J(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}),z(this.memorySearch);re.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 re.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(B(n))return re.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;re.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}=H(h,e);if(s)return re.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();re.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)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g);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 k(()=>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=te(this.config,i),a=o(r?.name??se(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 j(()=>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=Z(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]),ee(e),t(e,ne(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 K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new P(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=te(this.config,t),o=s?te(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??se(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?se(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 D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new F(e=>this.agentService.getUsage(e)))}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){re.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,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){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";re.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&re.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),o=await this.messageProcessor.process(e),i=p(o,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});re.debug(`[${t}] Prompt to agent (${i.text.length} chars): ${this.config.verboseDebugLogs?i.text:i.text.slice(0,15)+"..."}${i.images.length>0?` [+${i.images.length} image(s)]`:""}`);const r=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},c=y(this.config.dataDir),h={config:this.config,sessionContext:a,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(h),l=b({...h,mode:"minimal"});re.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,i,n.sessionId,g,l,r,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=ie.analyzeError(e.error,e),o=ie.getRecoveryStrategy(s);return re.warn(`[${t}] ${o.message}`),this.sessionManager.updateSessionId(t,""),o.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(re.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(i.text||"[media]").trim();await this.memoryManager.append(t,"user",e,o.savedFiles.length>0?o.savedFiles:void 0),await this.memoryManager.append(t,"assistant",X(s.fullResponse??s.response))}if("max_turns"===s.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 s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.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 s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(re.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(re.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){re.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"),re.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{re.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.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}),re.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}),re.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?re.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?re.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):re.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(){re.info("Trigger restart requested");const e=Y();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();re.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),re.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){re.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&&(re.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>re.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){re.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){re.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}re.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){re.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new u(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.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const o=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,i=this.cronService?Q(this.cronService,()=>this.config):void 0,r=e.tts.enabled?W(()=>this.config):void 0,a=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(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;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,i,this.sessionDb,r,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,o,a),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),re.info("Server reconfigured successfully")}async stop(){re.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),re.info("Server stopped")}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as i,resolve as o}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 S,loadWorkspaceFiles as y}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 k,DefaultModelCommand as x}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as I}from"./commands/mcp.js";import{ModelsCommand as D}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as P}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as F}from"./commands/usage.js";import{CronService as O}from"./cron/cron-service.js";import{stripHeartbeatToken as B,isHeartbeatContentEffectivelyEmpty as H}from"./cron/heartbeat-token.js";import{createServerToolsServer as L}from"./tools/server-tools.js";import{createCronToolsServer as Q}from"./tools/cron-tools.js";import{createTTSToolsServer as W}from"./tools/tts-tools.js";import{createMemoryToolsServer as z}from"./tools/memory-tools.js";import{createBrowserToolsServer as G}from"./tools/browser-tools.js";import{createPicoToolsServer as q}from"./tools/pico-tools.js";import{BrowserService as V}from"./browser/browser-service.js";import{MemorySearch as J}from"./memory/memory-search.js";import{stripMediaLines as X}from"./utils/media-response.js";import{loadConfig as Y,loadRawConfig as Z,backupConfig as ee,resolveModelEntry as te,modelRefName as se}from"./config.js";import{stringify as ne}from"yaml";import{createLogger as ie}from"./utils/logger.js";import{SessionErrorHandler as oe}from"./agent/session-error-handler.js";import{initStickerCache as re}from"./gateway/channels/telegram/stickers.js";const ae=ie("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=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),re(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const o=this.createMemorySearch(e);this.browserService=new V;const g=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?Q(this.cronService,()=>this.config):void 0,m=e.tts.enabled?W(()=>this.config):void 0,d=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(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;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,m,o,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g,d),S(e.dataDir),s(i(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(i(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(i(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 O({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=te(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",i=s?.baseURL||"";if(n)return this.memorySearch=new J(e.memoryDir,e.dataDir,{apiKey:n,baseURL:i||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}),z(this.memorySearch);ae.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 i=`${s}:${n}`;e.has(i)||(e.add(i),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),i=e.sessionKey.substring(t+1);"cron"!==n&&i&&(this.channelManager.getAdapter(n)&&s(n,i))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return ae.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=i(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(H(n))return ae.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const o="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=o?"cron":t.channel,a=o?t.name:t.chatId;ae.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}=B(h,e);if(s)return ae.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();ae.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 k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,i=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),o=this.sessionManager.getModel(e)||this.config.agent.model,r=te(this.config,o),a=i(r?.name??se(o)),c=i(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 x(()=>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 i=this.config.agent.picoAgent;if(i?.enabled&&Array.isArray(i.modelRefs)){const t=s?.name??e,n=i.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=i.modelRefs.splice(n,1);i.modelRefs.unshift(e)}}try{const e=o(process.cwd(),"config.yaml"),s=Z(e);s.agent||(s.agent={}),s.agent.model=n,i?.enabled&&Array.isArray(i.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...i.modelRefs]),ee(e),t(e,ne(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new D(()=>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 K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new P(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=te(this.config,t),i=s?te(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??se(t),fallbackModel:i?.id??s,fallbackModelName:i?.name??(s?se(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 j(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new I(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new F(e=>this.agentService.getUsage(e)))}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){ae.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){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";ae.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&ae.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.buttons&&s.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,s.text,s.buttons),""):s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),i=await this.messageProcessor.process(e),o=u(i,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});ae.debug(`[${t}] Prompt to agent (${o.text.length} chars): ${this.config.verboseDebugLogs?o.text:o.text.slice(0,15)+"..."}${o.images.length>0?` [+${o.images.length} image(s)]`:""}`);const r=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},c=y(this.config.dataDir),h={config:this.config,sessionContext:a,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(h),l=b({...h,mode:"minimal"});ae.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,o,n.sessionId,g,l,r,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return ae.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=oe.analyzeError(e.error,e),i=oe.getRecoveryStrategy(s);return ae.warn(`[${t}] ${i.message}`),this.sessionManager.updateSessionId(t,""),i.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(ae.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(o.text||"[media]").trim();await this.memoryManager.append(t,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(t,"assistant",X(s.fullResponse??s.response))}if("max_turns"===s.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 s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.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 s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(ae.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(ae.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){ae.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"),ae.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{ae.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),ae.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}),ae.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}),ae.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?ae.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?ae.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):ae.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(){ae.info("Trigger restart requested");const e=Y();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();ae.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),ae.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){ae.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&&(ae.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>ae.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){ae.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),i=s.substring(n+1),o=this.channelManager.getAdapter(t);if(o)try{await o.sendText(i,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){ae.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}ae.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){ae.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.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const i=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,o=this.cronService?Q(this.cronService,()=>this.config):void 0,r=e.tts.enabled?W(()=>this.config):void 0,a=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(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;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,o,this.sessionDb,r,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,i,a),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),ae.info("Server reconfigured successfully")}async stop(){ae.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),ae.info("Server stopped")}}
@@ -1 +1 @@
1
- import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("CronTools");export function createCronToolsServer(o,s){return e({name:"cron-tools",version:"1.0.0",tools:[t("cron_add","Create a new cron job. The job will send the specified message to the agent on the given schedule. Channel and chatId are taken from the current session (from <session_info>). The isolated setting is inherited from the global cron config.",{name:n.string().describe("Unique name for the job (e.g. 'daily-reminder')"),description:n.string().optional().describe("Optional description of what the job does"),message:n.string().describe("The message text to send to the agent when the job fires"),channel:n.string().describe("The channel to deliver responses to (from <session_info>)"),chatId:n.string().describe("The chat ID to deliver responses to (from <session_info>)"),scheduleKind:n.enum(["every","cron","at"]).describe("Schedule type: 'every' for interval, 'cron' for cron expression, 'at' for one-shot"),everyMs:n.number().optional().describe("Interval in milliseconds (required when scheduleKind is 'every')"),cronExpr:n.string().optional().describe("Cron expression like '0 9 * * *' (required when scheduleKind is 'cron')"),at:n.string().optional().describe("ISO datetime for one-shot execution (required when scheduleKind is 'at')"),suppressToken:n.boolean().optional().describe("If true, suppress HEARTBEAT_OK responses (default: false)")},async e=>{try{const t=s();let n;if("every"===e.scheduleKind){if(!e.everyMs)return{content:[{type:"text",text:"everyMs is required for 'every' schedule"}],isError:!0};n={kind:"every",everyMs:e.everyMs}}else if("cron"===e.scheduleKind){if(!e.cronExpr)return{content:[{type:"text",text:"cronExpr is required for 'cron' schedule"}],isError:!0};n={kind:"cron",expr:e.cronExpr}}else{if(!e.at)return{content:[{type:"text",text:"at is required for 'at' schedule"}],isError:!0};n={kind:"at",at:e.at}}const i=await o.add({name:e.name,description:e.description,channel:e.channel,chatId:e.chatId,message:e.message,schedule:n,isolated:t.cron.isolated,suppressToken:e.suppressToken??!1,enabled:!0});return r.info(`Cron job created by agent: "${i.name}" (${i.id})`),{content:[{type:"text",text:`Job "${i.name}" created (id: ${i.id}). Schedule: ${JSON.stringify(n)}. Isolated: ${t.cron.isolated}. Next run: ${i.state.nextRunAtMs?new Date(i.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return r.error(`cron_add failed: ${t}`),{content:[{type:"text",text:`Failed to create job: ${t}`}],isError:!0}}}),t("cron_list","List all cron jobs with their status, schedule, and next run time.",{includeDisabled:n.boolean().optional().describe("Include disabled jobs (default: false)")},async e=>{try{const t=await o.list({includeDisabled:e.includeDisabled??!1});if(0===t.length)return{content:[{type:"text",text:"No cron jobs found."}]};const n=t.map(e=>({id:e.id,name:e.name,enabled:e.enabled,isolated:e.isolated,schedule:e.schedule,channel:e.channel,chatId:e.chatId,nextRun:e.state.nextRunAtMs?new Date(e.state.nextRunAtMs).toISOString():null,lastStatus:e.state.lastStatus??null}));return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){return{content:[{type:"text",text:`Failed to list jobs: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_remove","Delete a cron job by its ID.",{id:n.string().describe("The job ID to delete")},async e=>{try{return(await o.remove(e.id)).removed?(r.info(`Cron job removed by agent: ${e.id}`),{content:[{type:"text",text:`Job ${e.id} deleted.`}]}):{content:[{type:"text",text:`Job ${e.id} not found.`}],isError:!0}}catch(e){return{content:[{type:"text",text:`Failed to delete job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_update","Update an existing cron job. Only provide the fields you want to change.",{id:n.string().describe("The job ID to update"),name:n.string().optional().describe("New name for the job"),description:n.string().optional().describe("New description"),message:n.string().optional().describe("New message text"),enabled:n.boolean().optional().describe("Enable or disable the job"),suppressToken:n.boolean().optional().describe("Enable or disable HEARTBEAT_OK suppression")},async e=>{try{const t={};void 0!==e.name&&(t.name=e.name),void 0!==e.description&&(t.description=e.description),void 0!==e.message&&(t.message=e.message),void 0!==e.enabled&&(t.enabled=e.enabled),void 0!==e.suppressToken&&(t.suppressToken=e.suppressToken);const n=await o.update(e.id,t);return r.info(`Cron job updated by agent: "${n.name}" (${n.id})`),{content:[{type:"text",text:`Job "${n.name}" updated. Enabled: ${n.enabled}. Next run: ${n.state.nextRunAtMs?new Date(n.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){return{content:[{type:"text",text:`Failed to update job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("CronTools");export function createCronToolsServer(o,s){return e({name:"cron-tools",version:"1.0.0",tools:[t("cron_add","Create a new cron job. The job will send the specified message to the agent on the given schedule. Channel and chatId are taken from the current session (from <session_info>). The isolated setting is inherited from the global cron config.",{name:n.string().describe("Unique name for the job (e.g. 'daily-reminder')"),description:n.string().optional().describe("Optional description of what the job does"),message:n.string().describe("The message text to send to the agent when the job fires"),channel:n.string().describe("The channel to deliver responses to (from <session_info>)"),chatId:n.string().describe("The chat ID to deliver responses to (from <session_info>)"),scheduleKind:n.enum(["every","cron","at"]).describe("Schedule type: 'every' for interval, 'cron' for cron expression, 'at' for one-shot"),everyMs:n.number().optional().describe("Interval in milliseconds (required when scheduleKind is 'every')"),cronExpr:n.string().optional().describe("Cron expression like '0 9 * * *' (required when scheduleKind is 'cron')"),at:n.string().optional().describe("ISO datetime for one-shot execution (required when scheduleKind is 'at')"),suppressToken:n.boolean().optional().describe("If true, suppress HEARTBEAT_OK responses (default: false)"),timeoutSeconds:n.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t=s();let n;if("every"===e.scheduleKind){if(!e.everyMs)return{content:[{type:"text",text:"everyMs is required for 'every' schedule"}],isError:!0};n={kind:"every",everyMs:e.everyMs}}else if("cron"===e.scheduleKind){if(!e.cronExpr)return{content:[{type:"text",text:"cronExpr is required for 'cron' schedule"}],isError:!0};n={kind:"cron",expr:e.cronExpr}}else{if(!e.at)return{content:[{type:"text",text:"at is required for 'at' schedule"}],isError:!0};n={kind:"at",at:e.at}}const i=await o.add({name:e.name,description:e.description,channel:e.channel,chatId:e.chatId,message:e.message,schedule:n,isolated:t.cron.isolated,suppressToken:e.suppressToken??!1,timeoutSeconds:e.timeoutSeconds,enabled:!0});return r.info(`Cron job created by agent: "${i.name}" (${i.id})`),{content:[{type:"text",text:`Job "${i.name}" created (id: ${i.id}). Schedule: ${JSON.stringify(n)}. Isolated: ${t.cron.isolated}. Next run: ${i.state.nextRunAtMs?new Date(i.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return r.error(`cron_add failed: ${t}`),{content:[{type:"text",text:`Failed to create job: ${t}`}],isError:!0}}}),t("cron_list","List all cron jobs with their status, schedule, and next run time.",{includeDisabled:n.boolean().optional().describe("Include disabled jobs (default: false)")},async e=>{try{const t=await o.list({includeDisabled:e.includeDisabled??!1});if(0===t.length)return{content:[{type:"text",text:"No cron jobs found."}]};const n=t.map(e=>({id:e.id,name:e.name,enabled:e.enabled,isolated:e.isolated,schedule:e.schedule,channel:e.channel,chatId:e.chatId,nextRun:e.state.nextRunAtMs?new Date(e.state.nextRunAtMs).toISOString():null,lastStatus:e.state.lastStatus??null}));return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}catch(e){return{content:[{type:"text",text:`Failed to list jobs: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_remove","Delete a cron job by its ID.",{id:n.string().describe("The job ID to delete")},async e=>{try{return(await o.remove(e.id)).removed?(r.info(`Cron job removed by agent: ${e.id}`),{content:[{type:"text",text:`Job ${e.id} deleted.`}]}):{content:[{type:"text",text:`Job ${e.id} not found.`}],isError:!0}}catch(e){return{content:[{type:"text",text:`Failed to delete job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),t("cron_update","Update an existing cron job. Only provide the fields you want to change.",{id:n.string().describe("The job ID to update"),name:n.string().optional().describe("New name for the job"),description:n.string().optional().describe("New description"),message:n.string().optional().describe("New message text"),enabled:n.boolean().optional().describe("Enable or disable the job"),suppressToken:n.boolean().optional().describe("Enable or disable HEARTBEAT_OK suppression"),timeoutSeconds:n.number().optional().describe("Per-job execution timeout in seconds. 0 = no timeout. Default: 600 (10 min)")},async e=>{try{const t={};void 0!==e.name&&(t.name=e.name),void 0!==e.description&&(t.description=e.description),void 0!==e.message&&(t.message=e.message),void 0!==e.enabled&&(t.enabled=e.enabled),void 0!==e.suppressToken&&(t.suppressToken=e.suppressToken),void 0!==e.timeoutSeconds&&(t.timeoutSeconds=e.timeoutSeconds);const n=await o.update(e.id,t);return r.info(`Cron job updated by agent: "${n.name}" (${n.id})`),{content:[{type:"text",text:`Job "${n.name}" updated. Enabled: ${n.enabled}. Next run: ${n.state.nextRunAtMs?new Date(n.state.nextRunAtMs).toISOString():"none"}.`}]}}catch(e){return{content:[{type:"text",text:`Failed to update job: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})]})}
@@ -1 +1 @@
1
- import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{return e.buttons&&e.buttons.length>0?await s.sendButtons(e.channel,e.chatId,e.text,e.buttons):await s.sendResponse(e.channel,e.chatId,e.text),a.info(`Message sent to ${e.channel}:${e.chatId} (${e.text.length} chars)`),{content:[{type:"text",text:`Message sent to ${e.channel}:${e.chatId}`}]}}catch(t){const n=t instanceof Error?t.message:String(t);return a.error(`Failed to send message to ${e.channel}:${e.chatId}: ${n}`),{content:[{type:"text",text:`Error sending message: ${n}`}],isError:!0}}}),t("list_models","List all models in the registry with their name, model ID, API base URL, and API key environment variable name.",{},async()=>{const e=(o().models||[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy});if(0===e.length)return{content:[{type:"text",text:"No models in registry."}]};const t=e.map(e=>({name:e.name,modelId:e.id,type:(e.types||["external"])[0],baseURL:e.baseURL||"",apiKeyEnvVar:e.useEnvVar||((e.types||["external"]).includes("internal")?"":"OPENAI_API_KEY")}));return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}),t("list_channels","List all registered channels, whether they are currently active, and known chat IDs for each channel (from config allowFrom + session history).",{},async()=>{const e=s.listAdapters();if(0===e.length)return{content:[{type:"text",text:"No channels registered."}]};const t=o(),n=r.listSessions(),a=e.map(e=>{const s=new Set,a=[],o=t.channels[e.name];if(o?.accounts)for(const e of Object.values(o.accounts)){const t=e?.allowFrom;if(Array.isArray(t))for(const e of t){const t=String(e).trim();t&&!s.has(t)&&(s.add(t),a.push({id:t,source:"config"}))}}for(const t of n){const n=t.sessionKey.indexOf(":");if(n<0)continue;const o=t.sessionKey.substring(0,n),r=t.sessionKey.substring(n+1);o===e.name&&r&&!s.has(r)&&(s.add(r),a.push({id:r,source:"session"}))}return{name:e.name,active:e.active,chatIds:a}});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}})]})}
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{return e.buttons&&e.buttons.length>0?await s.sendButtons(e.channel,e.chatId,e.text,[e.buttons]):await s.sendResponse(e.channel,e.chatId,e.text),a.info(`Message sent to ${e.channel}:${e.chatId} (${e.text.length} chars)`),{content:[{type:"text",text:`Message sent to ${e.channel}:${e.chatId}`}]}}catch(t){const n=t instanceof Error?t.message:String(t);return a.error(`Failed to send message to ${e.channel}:${e.chatId}: ${n}`),{content:[{type:"text",text:`Error sending message: ${n}`}],isError:!0}}}),t("list_models","List all models in the registry with their name, model ID, API base URL, and API key environment variable name.",{},async()=>{const e=(o().models||[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy});if(0===e.length)return{content:[{type:"text",text:"No models in registry."}]};const t=e.map(e=>({name:e.name,modelId:e.id,type:(e.types||["external"])[0],baseURL:e.baseURL||"",apiKeyEnvVar:e.useEnvVar||((e.types||["external"]).includes("internal")?"":"OPENAI_API_KEY")}));return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}),t("list_channels","List all registered channels, whether they are currently active, and known chat IDs for each channel (from config allowFrom + session history).",{},async()=>{const e=s.listAdapters();if(0===e.length)return{content:[{type:"text",text:"No channels registered."}]};const t=o(),n=r.listSessions(),a=e.map(e=>{const s=new Set,a=[],o=t.channels[e.name];if(o?.accounts)for(const e of Object.values(o.accounts)){const t=e?.allowFrom;if(Array.isArray(t))for(const e of t){const t=String(e).trim();t&&!s.has(t)&&(s.add(t),a.push({id:t,source:"config"}))}}for(const t of n){const n=t.sessionKey.indexOf(":");if(n<0)continue;const o=t.sessionKey.substring(0,n),r=t.sessionKey.substring(n+1);o===e.name&&r&&!s.has(r)&&(s.add(r),a.push({id:r,source:"session"}))}return{name:e.name,active:e.active,chatIds:a}});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}})]})}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Telegram Actions MCP Tool Server
3
+ *
4
+ * Provides MCP tools for advanced Telegram features:
5
+ * - Message reactions
6
+ * - Edit and delete messages
7
+ * - Sticker search and sending
8
+ * - Poll creation
9
+ */
10
+ import type { AppConfig } from "../config.js";
11
+ import type { ChannelManager } from "../gateway/channel-manager.js";
12
+ export declare function createTelegramActionsToolsServer(channelManager: ChannelManager, getConfig: () => AppConfig): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
13
+ //# sourceMappingURL=telegram-actions-tools.d.ts.map
@@ -0,0 +1 @@
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as r}from"zod";import{createLogger as s}from"../utils/logger.js";import{reactMessageTelegram as o}from"../gateway/channels/telegram/reactions.js";import{editMessageTelegram as a,deleteMessageTelegram as n}from"../gateway/channels/telegram/edit-delete.js";import{searchStickers as c,sendStickerTelegram as i}from"../gateway/channels/telegram/stickers.js";import{sendPollTelegram as l}from"../gateway/channels/telegram/polls.js";const m=s("TelegramActionsTools");function d(e,t){const r=e.channels.telegram?.accounts;if(!r)throw new Error("Telegram is not configured");if(t){const e=r[t];if(!e)throw new Error(`Telegram account '${t}' not found`);return e.botToken}const s=function(e){const t=e.channels.telegram?.accounts;if(!t)return null;const r=Object.keys(t);if(0===r.length)return null;const s=t[r[0]];return s?.botToken??null}(e);if(!s)throw new Error("No Telegram accounts configured");return s}export function createTelegramActionsToolsServer(s,g){return e({name:"telegram-actions",version:"1.0.0",tools:[t("telegram_react","React to a Telegram message with an emoji. Use sparingly - check reactionLevel config first.",{chatId:r.string().describe("Telegram chat ID where the message is located"),messageId:r.number().describe("Telegram message ID to react to"),emoji:r.string().describe("Emoji to react with (e.g., '👍', '❤️', '🔥')"),remove:r.boolean().optional().describe("Set to true to remove the reaction"),accountId:r.string().optional().describe("Telegram account ID (optional, uses default if not specified)")},async e=>{try{const t=d(g(),e.accountId),r=await o(e.chatId,e.messageId,e.emoji,t,{remove:e.remove});return r.ok?(m.info(`Reacted to message ${e.messageId} with ${e.emoji}`),{content:[{type:"text",text:e.remove?`Removed reaction from message ${e.messageId}`:`Reacted to message ${e.messageId} with ${e.emoji}`}]}):(m.warn(`Reaction failed: ${r.warning}`),{content:[{type:"text",text:`Warning: ${r.warning}`}]})}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to react to message: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_edit_message","Edit a previously sent Telegram message. Can update text and/or buttons.",{chatId:r.string().describe("Telegram chat ID where the message is located"),messageId:r.number().describe("Telegram message ID to edit"),text:r.string().describe("New message text (markdown format)"),buttons:r.array(r.array(r.object({text:r.string().describe("Button label"),callback_data:r.string().describe("Data sent back when button is tapped")}))).optional().describe("Optional inline buttons (array of button rows). Pass empty array to remove buttons."),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId);await a(e.chatId,e.messageId,e.text,t,{buttons:e.buttons});return m.info(`Edited message ${e.messageId} in chat ${e.chatId}`),{content:[{type:"text",text:`Message ${e.messageId} edited successfully`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to edit message: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_delete_message","Delete a Telegram message.",{chatId:r.string().describe("Telegram chat ID where the message is located"),messageId:r.number().describe("Telegram message ID to delete"),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId);return await n(e.chatId,e.messageId,t),m.info(`Deleted message ${e.messageId} from chat ${e.chatId}`),{content:[{type:"text",text:`Message ${e.messageId} deleted successfully`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to delete message: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_search_stickers","Search cached stickers by text query (emoji, description, or set name). Returns top matching stickers with their file IDs.",{query:r.string().describe("Search query (e.g., 'happy', '👍', 'cat')"),limit:r.number().default(5).describe("Maximum number of results (default: 5)")},async e=>{try{const t=c(e.query,e.limit);if(0===t.length)return{content:[{type:"text",text:`No stickers found matching "${e.query}"`}]};const r=t.map((e,t)=>`${t+1}. ${e.emoji??"📄"} ${e.description}\n File ID: ${e.fileId}${e.setName?`\n Set: ${e.setName}`:""}`).join("\n\n");return m.info(`Found ${t.length} stickers matching "${e.query}"`),{content:[{type:"text",text:`Found ${t.length} sticker(s):\n\n${r}`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to search stickers: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_send_sticker","Send a sticker to a Telegram chat using its file ID (from telegram_search_stickers).",{chatId:r.string().describe("Telegram chat ID to send to"),fileId:r.string().describe("Telegram sticker file ID (from search results)"),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId),r=await i(e.chatId,e.fileId,t);return m.info(`Sent sticker to chat ${e.chatId}`),{content:[{type:"text",text:`Sticker sent to chat ${e.chatId} (message ID: ${r.messageId})`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to send sticker: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}}),t("telegram_send_poll","Create a poll in a Telegram chat.",{chatId:r.string().describe("Telegram chat ID to send poll to"),question:r.string().describe("Poll question (max 300 chars)"),options:r.array(r.string()).describe("Poll options (2-10 options, each max 100 chars)"),maxSelections:r.number().default(1).describe("Maximum number of options users can select (default: 1 for single-choice poll)"),durationSeconds:r.number().optional().describe("Poll duration in seconds (5-600, optional)"),isAnonymous:r.boolean().default(!0).describe("Whether votes are anonymous (default: true)"),accountId:r.string().optional().describe("Telegram account ID (optional)")},async e=>{try{const t=d(g(),e.accountId),r=await l(e.chatId,{question:e.question,options:e.options,maxSelections:e.maxSelections,durationSeconds:e.durationSeconds,isAnonymous:e.isAnonymous},t);return m.info(`Sent poll to chat ${e.chatId}`),{content:[{type:"text",text:`Poll created in chat ${e.chatId} (message ID: ${r.messageId})`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return m.error(`Failed to send poll: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}})]})}
@@ -22,6 +22,51 @@ channels:
22
22
  dmPolicy: "allowlist"
23
23
  allowFrom: [] # telegram user IDs allowed (for allowlist policy)
24
24
 
25
+ # ─── Advanced Features (optional) ───────────────────────────────────
26
+ # name: "Main Bot" # optional display name for multi-account setups
27
+
28
+ # reactionLevel: "ack" # agent reaction capability (default: "ack")
29
+ # off - no reactions
30
+ # ack - bot shows 👀 while processing
31
+ # minimal - agent can react sparingly (~1 per 5-10 messages)
32
+ # extensive - agent can react liberally when appropriate
33
+
34
+ # reactionNotifications: "off" # user reaction notifications (default: "off")
35
+ # off - ignore all reactions
36
+ # own - notify when users react to bot messages
37
+ # all - notify agent of all reactions
38
+
39
+ # inlineButtonsScope: "allowlist" # where inline buttons are allowed (default: "allowlist")
40
+ # off - buttons disabled everywhere
41
+ # dm - buttons only in direct messages
42
+ # group - buttons only in groups
43
+ # all - buttons allowed everywhere
44
+ # allowlist - buttons based on allowlist
45
+
46
+ # textChunkLimit: 4000 # max message length before splitting (default: 4000)
47
+ # streamMode: "partial" # message streaming mode (default: "partial")
48
+ # off - no streaming
49
+ # partial - stream text before tool calls
50
+ # block - stream block-by-block
51
+
52
+ # linkPreview: true # show link previews in messages (default: true)
53
+
54
+ # actions: # enable/disable specific actions (default: all true except sticker)
55
+ # reactions: true # allow message reactions
56
+ # sendMessage: true # allow sending messages
57
+ # editMessage: true # allow editing messages
58
+ # deleteMessage: true # allow deleting messages
59
+ # sticker: false # enable sticker cache & search (default: false)
60
+ # createForumTopic: false # allow creating forum topics (default: false)
61
+
62
+ # retry: # retry policy for API calls (default: 3 attempts, 1s base delay)
63
+ # maxAttempts: 3
64
+ # baseDelayMs: 1000
65
+ # maxDelayMs: 30000
66
+
67
+ # timeoutSeconds: 60 # API timeout (optional)
68
+ # proxy: "socks5://..." # SOCKS5 proxy for Telegram API (optional)
69
+
25
70
  # whatsapp:
26
71
  # enabled: false
27
72
  # accounts:
@@ -134,6 +179,8 @@ memory:
134
179
  enabled: false # enable hybrid BM25 + semantic search over memory
135
180
  modelRef: "" # references a model in the models registry (for API key + base URL)
136
181
  embeddingModel: "text-embedding-3-small"
182
+ prefixQuery: "" # prefix template for query embeddings (use {content} placeholder)
183
+ prefixDocument: "" # prefix template for document embeddings (use {content} placeholder)
137
184
  embeddingDimensions: 1536 # 512, 768, or 1536 (native) — lower = faster, higher = more precise
138
185
  updateDebounceMs: 3000 # debounce for fs.watch before re-indexing
139
186
  embedIntervalMs: 300000 # interval (ms) between embedding cycles (5 min)
@@ -141,10 +188,16 @@ memory:
141
188
  maxSnippetChars: 700 # max chars per snippet in results
142
189
  maxInjectedChars: 4000 # max total chars injected into context from search results
143
190
  rrfK: 60 # RRF fusion constant (higher = smoother blending)
191
+ dir: "./memory" # memory storage directory (relative to gmabPath/data)
144
192
 
145
193
  agent:
146
- model: "Claude Opus" # model name or ID from the models registry
194
+ model: "Claude Opus" # model name or ID from the models registry (format: "Name:id")
147
195
  mainFallback: "" # fallback model if primary fails (name or ID)
196
+ # picoAgent — use external LLM providers (OpenRouter, OpenAI, etc.) via pi-ai instead of Claude SDK
197
+ picoAgent:
198
+ enabled: false
199
+ modelRefs: [] # list of "Name:provider:modelId" refs (e.g. "GPT:openrouter:openai/gpt-5.2")
200
+ rollingMemoryModel: "" # model for context summarization, full ref format (e.g. "MyModel:openrouter:openai/gpt-4.1-mini")
148
201
  maxTurns: 30
149
202
  permissionMode: "bypassPermissions"
150
203
  sessionTTL: 3600
@@ -265,6 +318,7 @@ agent:
265
318
  plugins: [] # local plugins, configurable via Nostromo UI
266
319
  inflightTyping: true # keep typing indicator active when multiple messages are in-flight
267
320
  autoApproveTools: true # auto-approve SDK tool permissions; when false, asks user via channel buttons
321
+ autoRenew: 0 # auto-renew session after N consecutive errors (0 = disabled)
268
322
 
269
323
  cron:
270
324
  enabled: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.6",
3
+ "version": "1.6.11",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",