@hera-al/server 1.6.36 → 1.6.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,formatWorkspaceFilesWithMeta as l,filterForSubagent as c}from"./workspace-files.js";import{createLogger as m}from"../utils/logger.js";const d=m("PromptBuilder");export function buildSystemPrompt(l){const{config:m,sessionContext:f,mode:E,hasNodeTools:y,hasMessageTools:_,coderSkill:I,subagentTask:O,toolServers:A}=l,R="minimal"===E?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",N=a(m.dataDir,R),w="minimal"===E?c(l.workspaceFiles):l.workspaceFiles,b=resolvePlaceholders(N,{SESSION_KEY:f.sessionKey,CHANNEL:f.channel,CHAT_ID:f.chatId,SESSION_ID:f.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:f.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:f.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:O??"",NODE_TOOLS_INSTRUCTIONS:y?h():"",MESSAGE_TOOLS_INSTRUCTIONS:_?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?p(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:I?"":r(m.dataDir,E),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?g():"",AVAILABLE_TOOLS:S(A),RUNTIME_LINE:T(m,f),WORKSPACE_FILES:i(w,m.dataDir)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${E}, template=${R}, length=${b.length})`),b}export function buildSystemPromptWithMeta(i){const{config:m,sessionContext:d,mode:f,hasNodeTools:E,hasMessageTools:y,coderSkill:_,subagentTask:I,toolServers:O}=i,A="minimal"===f?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",R=a(m.dataDir,A),N="minimal"===f?c(i.workspaceFiles):i.workspaceFiles,{formatted:w,meta:b}=l(N,m.dataDir);return{prompt:resolvePlaceholders(R,{SESSION_KEY:d.sessionKey,CHANNEL:d.channel,CHAT_ID:d.chatId,SESSION_ID:d.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:d.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:d.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:I??"",NODE_TOOLS_INSTRUCTIONS:E?h():"",MESSAGE_TOOLS_INSTRUCTIONS:y?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?p(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:_?"":r(m.dataDir,f),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?g():"",AVAILABLE_TOOLS:S(O),RUNTIME_LINE:T(m,d),WORKSPACE_FILES:w}).replace(/\n{3,}/g,"\n\n"),meta:b}}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function h(){return"# Remote Nodes\n\nYou have access to remote nodes — external machines that you can control. Use the node tools to discover and interact with them.\n\n## Available tools\n\n- **list_nodes**: Call this to see which nodes are currently connected. Returns each node's ID, name, platform, hostname, and available commands. Always call this first before trying to execute commands, so you know which nodes are online and what their IDs are.\n\n- **node_exec**: Execute a command on a specific node. You must provide the nodeId (from list_nodes), the command name, and its parameters.\n\n## Supported commands\n\n- **shell.run**: Run a shell command on the node. Params: { cmd: string, args?: string[], cwd?: string, timeout?: number, env?: Record<string,string> }. Returns { stdout, stderr, exitCode }.\n- **shell.which**: Check if a binary exists on the node. Params: { cmd: string }. Returns { path } or null.\n\n## Guidelines\n\n- Always call list_nodes first to discover available nodes and their IDs. Do not guess node IDs.\n- When a user asks to run something on a remote machine, a node, or a specific hostname, call list_nodes to see what's online, then use node_exec with the appropriate nodeId.\n- If multiple nodes are connected, ask the user which one to use when the intent is ambiguous.\n- If no nodes are connected, inform the user that no remote nodes are available.\n- Report command results clearly: show stdout, note any stderr, and mention non-zero exit codes."}function u(){return"# Messaging\n\nYou have tools to send messages to chat channels. Each message you process includes a <session_info> block with the current channel and chatId.\n\n## Available tools\n\n- **send_message**: Send a text message to a specific channel and chat. Use the channel and chatId from the session context to reply on the current conversation. You can also send to a different channel or chatId if instructed.\n\n- **list_channels**: List all registered channels. Returns each channel's name and whether it is active.\n\n## Guidelines\n\n- Use send_message when you need to proactively send a message outside of the normal response flow (e.g. notifications, forwarding, or sending to a different chat).\n- Your normal response text is already delivered to the user. Only use send_message for additional messages or cross-channel communication.\n- The channel and chatId from the session context identify the current conversation. Use them to send follow-up messages to the same chat.\n- If the user asks you to message someone on a different channel or chat, use the appropriate channel name and chatId.\n- Never spam or send unsolicited messages. Only send when explicitly asked or when it is clearly part of the task."}function p(e){return`# Heartbeats\n\nHeartbeat prompt: ${e}\nIf you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:\nHEARTBEAT_OK\nThe system treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack and may suppress it (not deliver to the user).\nIf something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.`}function g(){return"## Memory Search Tools\n\nYou have access to memory search tools for recalling past conversations and knowledge:\n\n- `memory_search` — semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, and score. No full file payload is returned.\n- `memory_get` — reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.\n\nUse `memory_search` to find relevant past context before answering questions that might relate to previous conversations. Use `memory_get` to read the full content of a memory file when a search snippet is not enough."}function S(e){if(!e||0===e.length)return"";const n=[];for(const t of e){const e=t,o=e.instance?._registeredTools;if(o)for(const[e,t]of Object.entries(o)){const o=t.description||"",s=t.inputSchema?.def?.shape,a=[];if(s)for(const[e,n]of Object.entries(s)){let t=n.type||"unknown";"ZodNumber"===t?t="number":"ZodString"===t?t="string":"ZodBoolean"===t?t="boolean":"ZodArray"===t?t="array":"ZodObject"===t?t="object":"ZodEnum"===t&&(t="enum");const o=n.isOptional?.()??!1;a.push(`${e}${o?"?":""}: ${t}`)}const r=a.length>0?`Params: { ${a.join(", ")} }`:"No parameters.",i=o.match(/\.\s*(Returns?\s.+?)\.?\s*$/i),l=i?i[1].trim():"";let c=`- **${e}**: ${i?o.slice(0,i.index).trim():o.trim()}. ${r}`;l&&(c+=`. ${l}.`),n.push(c)}}return 0===n.length?"":n.join("\n")}function T(a,r){return`host=${e()} | os=${n()} ${t()} (${o()}) | model=${s(a.agent.model)} | channel=${r.channel} | session=${r.sessionKey}`}export function buildPrompt(e,n,t,o){const s=[],a=[];if(o){const n=e.contentBlocks.find(e=>"text"===e.type)?.text??"",t=/^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(n),a=/Current time: /.test(n),r=n.startsWith("/");if(!t&&!a&&!r){const e=new Date,n=new Intl.DateTimeFormat("en-US",{timeZone:o,weekday:"short"}).format(e),t=e.toLocaleString("en-US",{timeZone:o,year:"numeric"}),a=e.toLocaleString("en-US",{timeZone:o,month:"2-digit"}),r=e.toLocaleString("en-US",{timeZone:o,day:"2-digit"}),i=e.toLocaleString("en-GB",{timeZone:o,hour:"2-digit",minute:"2-digit",hour12:!1});s.push(`[${n} ${t}-${a}-${r} ${i}]`)}}n&&s.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?s.push(n.text):"image"===n.type&&n.imageBase64&&a.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});const r=e.savedFiles.filter(e=>!e.endsWith(".tgs"));if(r.length>0){s.push(""),s.push("Files available in the current working directory:");for(const e of r)s.push(`- ${e}`)}return{text:s.join("\n"),images:a}}
1
+ import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,formatWorkspaceFilesWithMeta as l,filterForSubagent as c}from"./workspace-files.js";import{createLogger as m}from"../utils/logger.js";const d=m("PromptBuilder");export function buildSystemPrompt(l){const{config:m,sessionContext:f,mode:E,hasNodeTools:y,hasMessageTools:I,coderSkill:_,subagentTask:O,toolServers:A}=l,w="minimal"===E?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",N=a(m.dataDir,w),R="minimal"===E?c(l.workspaceFiles):l.workspaceFiles;var b;const M=resolvePlaceholders(N,{SESSION_KEY:f.sessionKey,CHANNEL:f.channel,CHAT_ID:f.chatId,SESSION_ID:f.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:f.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:f.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:O??"",NODE_TOOLS_INSTRUCTIONS:y?h():"",MESSAGE_TOOLS_INSTRUCTIONS:I?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?g(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:_?"":r(m.dataDir,E),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?p():"",AVAILABLE_TOOLS:S(A),MESH_INFO:m.channels.mesh.enabled?(b=m.channels.mesh.agentId,["## Mesh (Inter-Agent Communication)","",`You are connected to the Hera Mesh as **${b}**.`,"The mesh allows direct communication between agents (e.g. Dante ↔ Beatrice).","","- To see who's online: use the **mesh-agents** skill",'- To send a message: `send_message(channel="mesh", chatId="<agentId>", text="...")`',"- Incoming mesh messages appear as normal messages from channel=mesh",""].join("\n")):"",RUNTIME_LINE:T(m,f),WORKSPACE_FILES:i(R,m.dataDir)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${E}, template=${w}, length=${M.length})`),M}export function buildSystemPromptWithMeta(i){const{config:m,sessionContext:d,mode:f,hasNodeTools:E,hasMessageTools:y,coderSkill:I,subagentTask:_,toolServers:O}=i,A="minimal"===f?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",w=a(m.dataDir,A),N="minimal"===f?c(i.workspaceFiles):i.workspaceFiles,{formatted:R,meta:b}=l(N,m.dataDir);return{prompt:resolvePlaceholders(w,{SESSION_KEY:d.sessionKey,CHANNEL:d.channel,CHAT_ID:d.chatId,SESSION_ID:d.sessionId||"(new session)",MODEL:s(m.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:m.agent.workspacePath,DATA_DIR:m.dataDir,MEMORY_FILE:d.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:d.attachmentsDir||"(memory disabled)",TIMEZONE:m.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:m.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:_??"",NODE_TOOLS_INSTRUCTIONS:E?h():"",MESSAGE_TOOLS_INSTRUCTIONS:y?u():"",HEARTBEAT_INSTRUCTIONS:m.cron.enabled?g(m.cron.heartbeat.message):"",HEARTBEAT_PROMPT:m.cron.enabled?m.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:I?"":r(m.dataDir,f),SEARCH_IN_MEMORIES:"builtin-only"!==m.memory.recallStrategy?p():"",AVAILABLE_TOOLS:S(O),RUNTIME_LINE:T(m,d),WORKSPACE_FILES:R}).replace(/\n{3,}/g,"\n\n"),meta:b}}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function h(){return"# Remote Nodes\n\nYou have access to remote nodes — external machines that you can control. Use the node tools to discover and interact with them.\n\n## Available tools\n\n- **list_nodes**: Call this to see which nodes are currently connected. Returns each node's ID, name, platform, hostname, and available commands. Always call this first before trying to execute commands, so you know which nodes are online and what their IDs are.\n\n- **node_exec**: Execute a command on a specific node. You must provide the nodeId (from list_nodes), the command name, and its parameters.\n\n## Supported commands\n\n- **shell.run**: Run a shell command on the node. Params: { cmd: string, args?: string[], cwd?: string, timeout?: number, env?: Record<string,string> }. Returns { stdout, stderr, exitCode }.\n- **shell.which**: Check if a binary exists on the node. Params: { cmd: string }. Returns { path } or null.\n\n## Guidelines\n\n- Always call list_nodes first to discover available nodes and their IDs. Do not guess node IDs.\n- When a user asks to run something on a remote machine, a node, or a specific hostname, call list_nodes to see what's online, then use node_exec with the appropriate nodeId.\n- If multiple nodes are connected, ask the user which one to use when the intent is ambiguous.\n- If no nodes are connected, inform the user that no remote nodes are available.\n- Report command results clearly: show stdout, note any stderr, and mention non-zero exit codes."}function u(){return"# Messaging\n\nYou have tools to send messages to chat channels. Each message you process includes a <session_info> block with the current channel and chatId.\n\n## Available tools\n\n- **send_message**: Send a text message to a specific channel and chat. Use the channel and chatId from the session context to reply on the current conversation. You can also send to a different channel or chatId if instructed.\n\n- **list_channels**: List all registered channels. Returns each channel's name and whether it is active.\n\n## Guidelines\n\n- Use send_message when you need to proactively send a message outside of the normal response flow (e.g. notifications, forwarding, or sending to a different chat).\n- Your normal response text is already delivered to the user. Only use send_message for additional messages or cross-channel communication.\n- The channel and chatId from the session context identify the current conversation. Use them to send follow-up messages to the same chat.\n- If the user asks you to message someone on a different channel or chat, use the appropriate channel name and chatId.\n- Never spam or send unsolicited messages. Only send when explicitly asked or when it is clearly part of the task."}function g(e){return`# Heartbeats\n\nHeartbeat prompt: ${e}\nIf you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:\nHEARTBEAT_OK\nThe system treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack and may suppress it (not deliver to the user).\nIf something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.`}function p(){return"## Memory Search Tools\n\nYou have access to memory search tools for recalling past conversations and knowledge:\n\n- `memory_search` — semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, and score. No full file payload is returned.\n- `memory_get` — reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.\n\nUse `memory_search` to find relevant past context before answering questions that might relate to previous conversations. Use `memory_get` to read the full content of a memory file when a search snippet is not enough."}function S(e){if(!e||0===e.length)return"";const n=[];for(const t of e){const e=t,o=e.instance?._registeredTools;if(o)for(const[e,t]of Object.entries(o)){const o=t.description||"",s=t.inputSchema?.def?.shape,a=[];if(s)for(const[e,n]of Object.entries(s)){let t=n.type||"unknown";"ZodNumber"===t?t="number":"ZodString"===t?t="string":"ZodBoolean"===t?t="boolean":"ZodArray"===t?t="array":"ZodObject"===t?t="object":"ZodEnum"===t&&(t="enum");const o=n.isOptional?.()??!1;a.push(`${e}${o?"?":""}: ${t}`)}const r=a.length>0?`Params: { ${a.join(", ")} }`:"No parameters.",i=o.match(/\.\s*(Returns?\s.+?)\.?\s*$/i),l=i?i[1].trim():"";let c=`- **${e}**: ${i?o.slice(0,i.index).trim():o.trim()}. ${r}`;l&&(c+=`. ${l}.`),n.push(c)}}return 0===n.length?"":n.join("\n")}function T(a,r){return`host=${e()} | os=${n()} ${t()} (${o()}) | model=${s(a.agent.model)} | channel=${r.channel} | session=${r.sessionKey}`}export function buildPrompt(e,n,t,o){const s=[],a=[];if(o){const n=e.contentBlocks.find(e=>"text"===e.type)?.text??"",t=/^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(n),a=/Current time: /.test(n),r=n.startsWith("/");if(!t&&!a&&!r){const e=new Date,n=new Intl.DateTimeFormat("en-US",{timeZone:o,weekday:"short"}).format(e),t=e.toLocaleString("en-US",{timeZone:o,year:"numeric"}),a=e.toLocaleString("en-US",{timeZone:o,month:"2-digit"}),r=e.toLocaleString("en-US",{timeZone:o,day:"2-digit"}),i=e.toLocaleString("en-GB",{timeZone:o,hour:"2-digit",minute:"2-digit",hour12:!1});s.push(`[${n} ${t}-${a}-${r} ${i}]`)}}n&&s.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?s.push(n.text):"image"===n.type&&n.imageBase64&&a.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});const r=e.savedFiles.filter(e=>!e.endsWith(".tgs"));if(r.length>0){s.push(""),s.push("Files available in the current working directory:");for(const e of r)s.push(`- ${e}`)}return{text:s.join("\n"),images:a}}
package/dist/config.d.ts CHANGED
@@ -266,6 +266,13 @@ declare const AppConfigSchema: z.ZodObject<{
266
266
  enabled: z.ZodDefault<z.ZodBoolean>;
267
267
  accounts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodAny>>;
268
268
  }, z.core.$strip>>>;
269
+ mesh: z.ZodDefault<z.ZodOptional<z.ZodObject<{
270
+ enabled: z.ZodDefault<z.ZodBoolean>;
271
+ brokerUrl: z.ZodDefault<z.ZodString>;
272
+ agentId: z.ZodDefault<z.ZodString>;
273
+ privateKey: z.ZodDefault<z.ZodString>;
274
+ reconnectDelayMs: z.ZodDefault<z.ZodNumber>;
275
+ }, z.core.$strip>>>;
269
276
  responses: z.ZodDefault<z.ZodOptional<z.ZodObject<{
270
277
  enabled: z.ZodDefault<z.ZodBoolean>;
271
278
  port: z.ZodDefault<z.ZodNumber>;
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),j=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({telegram:j.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),D=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),I=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:D.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:I.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),F={enabled:!0,model:""},K=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W),l0:E.optional().default(F)}),q=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),G=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),O=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],_=f.array(G).default(O),B=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),N=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),X=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),$={type:"claudecode",piModelRef:""},V=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),H={enabled:!1,modelRefs:[],rollingMemoryModel:""},Q=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),Z={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},J=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:X.optional().default($),picoAgent:V.optional().default(H),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),q).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(N).default([]),plugins:f.array(B).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Q.optional().default(Z),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1})}),Y={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},ee={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W,l0:F},te={model:"claude-opus-4-6",mainFallback:"",engine:$,picoAgent:H,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:Z,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},ae=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),oe={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},ne=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:ae.optional().default(oe)}),le={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:oe},re=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),se={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ie=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:_.optional().default(O),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(Y),memory:K.optional().default(ee),agent:J.optional().default(te),cron:ne.optional().default(le),nostromo:re.optional().default(se),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function ue(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function de(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(de);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=de(o);return t}return e}const ce={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:Y,memory:ee,agent:{...te,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:le,nostromo:se};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ce});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=de(d(f)),m=ie.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=d(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return d(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const a=resolveModelEntry(e,t);return a?a.id:t}
1
+ import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as d}from"dotenv";import{parse as u,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),j=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({enabled:f.boolean().default(!1),brokerUrl:f.string().default("ws://127.0.0.1:3780/ws"),agentId:f.string().default(""),privateKey:f.string().default(""),reconnectDelayMs:f.number().default(5e3)}),S=f.object({telegram:j.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),mesh:C.optional().default({enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3}),responses:R.optional().default({enabled:!0,port:3e3})}),T=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),A=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),D=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":T.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":A.optional().default({binaryPath:"whisper",model:"base"})}),I=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),U=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),z=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),L=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:I.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:U.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:z.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),W=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),K={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),F={enabled:!0,model:""},q=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:W.optional().default(K),l0:E.optional().default(F)}),G=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),O=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),_=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],B=f.array(O).default(_),N=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),X=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),$=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),V={type:"claudecode",piModelRef:""},H=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),Q={enabled:!1,modelRefs:[],rollingMemoryModel:""},Z=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),J={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},Y=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:$.optional().default(V),picoAgent:H.optional().default(Q),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),G).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(X).default([]),plugins:f.array(N).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Z.optional().default(J),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1})}),ee={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},te={enabled:!0,recallStrategy:"builtin-only",dir:"",search:K,l0:F},ae={model:"claude-opus-4-6",mainFallback:"",engine:V,picoAgent:Q,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0,apiRetry:J,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},oe=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),ne={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},le=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:oe.optional().default(ne)}),re={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:ne},se=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),ie={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},de=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:S.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},mesh:{enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3},responses:{enabled:!0,port:3004}}),models:B.optional().default(_),stt:D.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:L.optional().default(ee),memory:q.optional().default(te),agent:Y.optional().default(ae),cron:le.optional().default(re),nostromo:se.optional().default(ie),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function ue(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function ce(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(ce);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=ce(o);return t}return e}const fe={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:ee,memory:te,agent:{...ae,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:re,nostromo:ie};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(d({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...fe});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=ce(u(f)),m=de.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=u(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return u(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const a=resolveModelEntry(e,t);return a?a.id:t}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Hera Mesh Channel — inter-agent messaging via Hera Mesh broker.
3
+ *
4
+ * Each connected agent appears as a chatId. When Beatrice sends a message
5
+ * to Dante, it arrives as channel="mesh", chatId="beatrice".
6
+ * Dante can reply via sendText("beatrice", "...").
7
+ */
8
+ import type { ChannelAdapter, MessageHandler } from "../bridge.js";
9
+ export interface MeshChannelConfig {
10
+ brokerUrl: string;
11
+ agentId: string;
12
+ privateKey: string;
13
+ reconnectDelayMs?: number;
14
+ }
15
+ export declare class MeshChannel implements ChannelAdapter {
16
+ readonly name = "mesh";
17
+ private ws;
18
+ private config;
19
+ private onMessage;
20
+ private sessionToken;
21
+ private connected;
22
+ private reconnectTimer;
23
+ private reconnectDelay;
24
+ private stopping;
25
+ private pingTimer;
26
+ /** Track message IDs we sent, so we can recognize replies and not loop. */
27
+ private pendingOutbound;
28
+ constructor(config: MeshChannelConfig);
29
+ start(onMessage: MessageHandler): Promise<void>;
30
+ private connect;
31
+ /**
32
+ * Convert an incoming mesh message into an IncomingMessage and dispatch it.
33
+ *
34
+ * Anti-loop logic: if the message is a reply (replyTo) to something we sent,
35
+ * it's a response — dispatch to agent but do NOT auto-send the agent's reply back.
36
+ * If it's a new message (no replyTo matching our outbound), dispatch AND auto-reply.
37
+ */
38
+ private handleMeshMessage;
39
+ sendText(chatId: string, text: string): Promise<void>;
40
+ /**
41
+ * Send a typed mesh message (for MCP tools).
42
+ */
43
+ sendTyped(to: string, type: string, payload: unknown, replyTo?: string): string;
44
+ getSessionToken(): string | null;
45
+ getAgentId(): string;
46
+ getBrokerHttpUrl(): string;
47
+ isConnected(): boolean;
48
+ /** Known peers from presence events. Updated in real-time. */
49
+ private knownPeers;
50
+ /**
51
+ * Fetch the list of agents from the broker HTTP API.
52
+ * Returns agent IDs with their online/offline status.
53
+ */
54
+ getAgents(): Promise<Array<{
55
+ agentId: string;
56
+ status: string;
57
+ }>>;
58
+ /**
59
+ * Get a summary string for system prompt injection.
60
+ */
61
+ getMeshPromptInfo(): Promise<string>;
62
+ private sendRaw;
63
+ private startPing;
64
+ private stopPing;
65
+ private scheduleReconnect;
66
+ stop(): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=mesh.d.ts.map
@@ -0,0 +1 @@
1
+ import e from"node:crypto";import{WebSocket as t}from"ws";import{createLogger as n}from"../../utils/logger.js";const s=n("Mesh");function o(t,n){const s=function(t){const n=Buffer.from(t,"hex"),s=Buffer.from("302e020100300506032b657004220420","hex"),o=Buffer.concat([s,n]);return e.createPrivateKey({key:o,format:"der",type:"pkcs8"})}(n),o=Buffer.from(t,"hex");return e.sign(null,o,s).toString("hex")}export class MeshChannel{name="mesh";ws=null;config;onMessage=null;sessionToken=null;connected=!1;reconnectTimer=null;reconnectDelay;stopping=!1;pingTimer=null;pendingOutbound=new Set;constructor(e){this.config=e,this.reconnectDelay=e.reconnectDelayMs??5e3}async start(e){this.onMessage=e,this.stopping=!1,await this.connect()}connect(){return new Promise((e,n)=>{const{brokerUrl:i,agentId:r}=this.config;s.info(`Connecting to mesh broker at ${i} as ${r}...`);try{this.ws=new t(i)}catch(t){return s.error(`Failed to create WebSocket: ${t}`),this.scheduleReconnect(),void e()}let a=!1;const c=t=>{a||(a=!0,t?(s.error(`Mesh connection failed: ${t.message}`),this.scheduleReconnect(),e()):e())},h=setTimeout(()=>{c(new Error("Connection timeout")),this.ws?.close()},1e4);this.ws.on("open",()=>{}),this.ws.on("message",e=>{let t;try{t=JSON.parse(e.toString())}catch{return void s.warn("Invalid JSON from mesh broker")}switch(t.type){case"challenge":{const e=o(t.challenge,this.config.privateKey);this.sendRaw({type:"auth",agentId:this.config.agentId,signature:e,timestamp:Date.now()});break}case"auth_result":clearTimeout(h),t.authenticated&&t.sessionToken?(this.connected=!0,this.sessionToken=t.sessionToken,s.info(`Authenticated as ${this.config.agentId} on mesh`),this.startPing(),c()):c(new Error(`Auth failed: ${t.error??"unknown"}`));break;case"message":this.handleMeshMessage(t.message),this.sendRaw({type:"ack",messageId:t.message.id});break;case"presence":s.info(`Mesh presence: ${t.agentId} is ${t.status}`),this.knownPeers.set(t.agentId,t.status);break;case"ping":this.sendRaw({type:"pong"});break;case"pong":case"ack":break;case"error":s.warn(`Mesh error: ${t.error}`)}}),this.ws.on("close",(e,t)=>{clearTimeout(h),this.connected=!1,this.sessionToken=null,this.stopPing(),s.info(`Mesh connection closed (code=${e}, reason=${t?.toString()??"?"})`),this.stopping||this.scheduleReconnect(),c()}),this.ws.on("error",e=>{s.error(`Mesh WebSocket error: ${e.message}`)})})}async handleMeshMessage(t){if(!this.onMessage)return;const n=t.from,o=null!=t.replyTo&&this.pendingOutbound.has(t.replyTo);let i;if(t.replyTo&&this.pendingOutbound.has(t.replyTo)&&this.pendingOutbound.delete(t.replyTo),"string"==typeof t.payload)i=t.payload;else if(t.payload&&"object"==typeof t.payload){const e=t.payload;i="string"==typeof e.text?e.text:`[Mesh ${t.type}] ${JSON.stringify(t.payload)}`}else i=`[Mesh ${t.type}]`;const r={chatId:n,userId:t.from,channelName:"mesh",text:i,attachments:[],username:t.from};o?s.info(`Reply from ${t.from} (to outbound ${t.replyTo}) → dispatching to agent (no auto-reply)`):s.info(`New message from ${t.from}: ${t.type} → dispatching to agent (will auto-reply)`);try{const i=await this.onMessage(r);if(!o&&i&&i.trim()){const o={id:e.randomUUID(),from:this.config.agentId,to:n,type:"text",payload:{text:i},timestamp:Date.now(),replyTo:t.id};this.sendRaw({type:"message",message:o}),s.debug(`Auto-replied to ${t.from}: ${i.slice(0,80)}...`)}}catch(e){s.error(`Error handling mesh message from ${t.from}: ${e}`)}}async sendText(t,n){if(!this.connected||!this.ws)return void s.warn(`Cannot send to ${t}: mesh not connected`);const o=e.randomUUID(),i={id:o,from:this.config.agentId,to:t,type:"text",payload:{text:n},timestamp:Date.now()};this.pendingOutbound.add(o),this.sendRaw({type:"message",message:i}),s.debug(`Sent to ${t} (id=${o}): ${n.slice(0,80)}...`)}sendTyped(t,n,s,o){if(!this.connected||!this.ws)throw new Error("Mesh not connected");const i={id:e.randomUUID(),from:this.config.agentId,to:t,type:n,payload:s,timestamp:Date.now(),replyTo:o};return this.sendRaw({type:"message",message:i}),i.id}getSessionToken(){return this.sessionToken}getAgentId(){return this.config.agentId}getBrokerHttpUrl(){return this.config.brokerUrl.replace("ws://","http://").replace("wss://","https://").replace("/ws","")}isConnected(){return this.connected}knownPeers=new Map;async getAgents(){try{const e=this.getBrokerHttpUrl(),t=await fetch(`${e}/api/agents`,{headers:this.sessionToken?{Authorization:`Bearer ${this.sessionToken}`}:{}});if(!t.ok)throw new Error(`HTTP ${t.status}`);const n=await t.json();return Array.isArray(n.agents)?n.agents:[]}catch(e){s.warn(`Failed to fetch agents: ${e}`);const t=[];for(const[e,n]of this.knownPeers)t.push({agentId:e,status:n});return t}}async getMeshPromptInfo(){const e=(await this.getAgents()).filter(e=>e.agentId!==this.config.agentId);if(0===e.length)return"";const t=e.map(e=>`- **${e.agentId}** (${e.status??"unknown"})`);return["## Mesh (Inter-Agent Communication)",`You are connected to the Hera Mesh as **${this.config.agentId}**.`,"Other agents on the mesh:",...t,"",'To message an agent: use send_message(channel="mesh", chatId="<agentId>", text="...").',"When you receive a mesh message, it appears as a normal incoming message from channel=mesh."].join("\n")}sendRaw(e){this.ws&&this.ws.readyState===t.OPEN&&this.ws.send(JSON.stringify(e))}startPing(){this.stopPing(),this.pingTimer=setInterval(()=>{this.connected&&this.sendRaw({type:"ping"})},3e4)}stopPing(){this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null)}scheduleReconnect(){this.stopping||this.reconnectTimer||(s.info(`Reconnecting in ${this.reconnectDelay}ms...`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect().catch(e=>{s.error(`Reconnect failed: ${e}`)})},this.reconnectDelay))}async stop(){this.stopping=!0,this.stopPing(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(1e3,"Channel stopping"),this.ws=null),this.connected=!1,s.info("Mesh channel stopped")}}
@@ -1 +1 @@
1
- export function renderModals(){return'\n\x3c!-- Restart confirmation modal --\x3e\n<div class="modal-overlay" id="restartModal">\n <div class="modal">\n <button class="close-btn" onclick="closeRestartModal()">&times;</button>\n <h3>Restart Server</h3>\n <p>This will restart all channels, agents, and services. Active conversations will be interrupted.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn" onclick="doRestart()">Restart</button>\n <button class="btn-ghost" onclick="closeRestartModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="placeholderRefModal">\n <div class="modal" style="max-width:620px;text-align:left;max-height:85vh;overflow-y:auto">\n <button class="close-btn" onclick="document.getElementById(\'placeholderRefModal\').classList.remove(\'open\')">&times;</button>\n <h3 style="margin-bottom:14px">Placeholder Reference</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">These placeholders are resolved at runtime when building the system prompt from templates.</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:40%">Placeholder</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><code>{{ATTACHMENTS_DIR}}</code></td><td>Path to the session attachments folder, or <code>(memory disabled)</code></td></tr>\n <tr><td><code>{{AVAILABLE_TOOLS}}</code></td><td>Auto-discovered list of all registered MCP tools with name, description, and parameters</td></tr>\n <tr><td><code>{{CHANNEL}}</code></td><td>Channel name, e.g. <code>telegram</code>, <code>responses</code></td></tr>\n <tr><td><code>{{CHAT_ID}}</code></td><td>Chat / conversation ID within the channel</td></tr>\n <tr><td><code>{{DATA_DIR}}</code></td><td>Absolute path to the data directory (workspace files, templates)</td></tr>\n <tr><td><code>{{HEARTBEAT_INSTRUCTIONS}}</code></td><td>Heartbeat/cron behavioral instructions, or empty if cron is disabled</td></tr>\n <tr><td><code>{{HEARTBEAT_PROMPT}}</code></td><td>Raw heartbeat prompt message from config</td></tr>\n <tr><td><code>{{HOSTNAME}}</code></td><td>Server hostname</td></tr>\n <tr><td><code>{{MEMORY_FILE}}</code></td><td>Path to the current conversation memory file, or <code>(memory disabled)</code></td></tr>\n <tr><td><code>{{MESSAGE_TOOLS_INSTRUCTIONS}}</code></td><td>Instructions for cross-channel messaging tools, or empty if not available</td></tr>\n <tr><td><code>{{MODEL}}</code></td><td>Active model ID, e.g. <code>claude-opus-4-6</code></td></tr>\n <tr><td><code>{{NODE_TOOLS_INSTRUCTIONS}}</code></td><td>Instructions for node remote-execution tools, or empty if no nodes connected</td></tr>\n <tr><td><code>{{OS}}</code></td><td>Operating system info (type, release, arch)</td></tr>\n <tr><td><code>{{RUNTIME_LINE}}</code></td><td>Composed runtime info line (host, OS, model, channel, session)</td></tr>\n <tr><td><code>{{SEARCH_IN_MEMORIES}}</code></td><td>Memory search tools instructions (<code>memory_search</code>, <code>memory_get</code>). Populated when Recall Strategy is not <code>builtin-only</code>, empty otherwise.</td></tr>\n <tr><td><code>{{SESSION_ID}}</code></td><td>SDK session ID for resumption, or <code>(new session)</code></td></tr>\n <tr><td><code>{{SESSION_KEY}}</code></td><td>Current session identifier, e.g. <code>telegram:12345</code></td></tr>\n <tr><td><code>{{SUBAGENT_TASK}}</code></td><td>Task description for subagent mode (empty for main agent)</td></tr>\n <tr><td><code>{{TIMEZONE}}</code></td><td>Server timezone, e.g. <code>Europe/Rome</code></td></tr>\n <tr><td><code>{{WORKSPACE_DIR}}</code></td><td>Absolute path to the agent workspace directory</td></tr>\n <tr><td><code>{{WORKSPACE_FILES}}</code></td><td>All workspace .md files inlined as <code>## FileName.md</code> sections</td></tr>\n </tbody>\n </table>\n\n <h4 style="margin:18px 0 8px;font-size:14px">File Includes</h4>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:10px">Workspace files can include other <code>.md</code> files from the data directory using inline placeholders. Includes are resolved once (no recursion).</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:40%">Syntax</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><code>{{FILE:name.md}}</code></td><td>Replaced with the full contents of <code>name.md</code> from the data directory. If the file is not found, a comment <code>&lt;!-- FILE:name.md not found --&gt;</code> is inserted instead. Only <code>[A-Za-z0-9_-]+.md</code> filenames are matched.</td></tr>\n </tbody>\n </table>\n <p style="font-size:12px;color:var(--text-muted);margin-top:6px">Example: place <code>{{FILE:BEHAVIOUR.md}}</code> inside <code>AGENTS.md</code> to inline BEHAVIOUR.md at that position.</p>\n\n <h4 style="margin:18px 0 8px;font-size:14px">Memory Budget (Hot/Cold)</h4>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:10px">When <code>MEMORY.md</code> exceeds 8000 chars, only sections wrapped in hot markers are included in the prompt. Cold content is omitted but remains searchable via <code>memory_search</code>.</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:40%">Marker</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><code>&lt;!-- hot --&gt;</code></td><td>Start of a hot section (included in prompt)</td></tr>\n <tr><td><code>&lt;!-- /hot --&gt;</code></td><td>End of a hot section</td></tr>\n </tbody>\n </table>\n <p style="font-size:12px;color:var(--text-muted);margin-top:6px">If no hot markers are found, the entire file is loaded (backward compatible). Multiple hot sections are supported and concatenated.</p>\n </div>\n</div>\n<div class="modal-overlay" id="agentHelpModal">\n <div class="modal" style="max-width:680px;text-align:left;max-height:85vh;overflow-y:auto">\n <button class="close-btn" onclick="document.getElementById(\'agentHelpModal\').classList.remove(\'open\')">&times;</button>\n <h3 style="margin-bottom:14px">Agent Configuration Reference</h3>\n\n <h4 style="margin:16px 0 8px;font-size:14px;color:var(--accent)">Main</h4>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:30%">Field</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><strong>Default Model</strong></td><td>The model used for all conversations. Must be one of the models defined in the Models section.</td></tr>\n <tr><td><strong>Fallback Model</strong></td><td>If the default model fails (API error, overloaded, etc.), the agent automatically retries with this model. Leave empty to disable fallback.</td></tr>\n </tbody>\n </table>\n\n <h4 style="margin:16px 0 8px;font-size:14px;color:var(--accent)">Configuration</h4>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:30%">Field</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><strong>Max Turns</strong></td><td>Maximum number of agentic turns (tool use round-trips) per single invocation. Prevents runaway loops. A typical value is 5&ndash;15.</td></tr>\n <tr><td><strong>Permission Mode</strong></td><td>Controls how the SDK handles tool permissions.<br><code>default</code> &mdash; ask the user before running tools.<br><code>bypassPermissions</code> &mdash; run all allowed tools without asking (recommended for automated agents).<br><code>plan</code> &mdash; the agent proposes a plan and waits for approval before executing.</td></tr>\n <tr><td><strong>Session TTL</strong></td><td>Time in seconds before an idle session expires. When a session expires, the next message starts a new conversation (no history carried over). Default: 3600 (1 hour).</td></tr>\n <tr><td><strong>Setting Sources</strong></td><td>Which SDK settings files to load.<br><code>nothing</code> &mdash; ignore all settings files.<br><code>user</code> &mdash; load user-level settings (~/.claude).<br><code>project</code> &mdash; load project-level settings from workspace.<br><code>both</code> &mdash; load both user and project settings.</td></tr>\n <tr><td><strong>Coder Skill</strong></td><td>When enabled, the builtin coding prompt is always prepended to the system prompt, adding coding-oriented instructions. When disabled, the system prompt is built entirely from your templates (SYSTEM_PROMPT.md).</td></tr>\n <tr><td><strong>Allowed Tools</strong></td><td>Which SDK tools the agent can use. Unchecked tools are not available to the agent. Common tools: <code>Read</code>, <code>Write</code>, <code>Edit</code> for file operations; <code>Bash</code> for shell commands; <code>Glob</code>/<code>Grep</code> for search; <code>WebSearch</code>/<code>WebFetch</code> for web access; <code>Task</code> for sub-agents; <code>Skill</code> for slash commands.</td></tr>\n <tr><td><strong>Auto Approve Tools</strong></td><td>Controls how tool permission requests from the SDK are handled.<br><strong>On (default)</strong> &mdash; all tool invocations are automatically approved. This prevents the SDK from blocking when <code>permissionMode</code> is set to <code>default</code>.<br><strong>Off</strong> &mdash; tool permission requests are forwarded to the user&rsquo;s channel as interactive messages with Approve/Deny buttons. The SDK waits until the user responds before proceeding.</td></tr>\n </tbody>\n </table>\n\n <h4 style="margin:16px 0 8px;font-size:14px;color:var(--accent)">Message Queue</h4>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:10px">Controls how incoming messages are handled while the agent is already processing a response.</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:30%">Field</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><strong>Queue Mode</strong></td><td>\n <code>queue</code> &mdash; Simple FIFO. Each message is processed one at a time in order. The user must wait for each response before the next message is handled.<br>\n <code>collect</code> &mdash; Messages sent while the agent is busy are buffered. After a debounce period, all buffered messages are merged into a single prompt and processed together.<br>\n <code>steer</code> &mdash; A new message interrupts the current processing. The agent stops its current response and immediately starts handling the new message, combining context from both.\n </td></tr>\n <tr><td><strong>Debounce (ms)</strong></td><td>Only applies in <code>collect</code> mode. After the last buffered message arrives, the system waits this many milliseconds before flushing the batch. This allows grouping rapid-fire messages into a single prompt. Set to 0 for immediate flush.</td></tr>\n <tr><td><strong>Queue Cap</strong></td><td>Maximum number of messages that can accumulate in the buffer. When this limit is reached, the Drop Policy kicks in. Set to 0 for unlimited.</td></tr>\n <tr><td><strong>Drop Policy</strong></td><td>What happens when Queue Cap is exceeded:<br>\n <code>new</code> &mdash; The incoming message is rejected; the user receives an error asking to wait.<br>\n <code>old</code> &mdash; The oldest buffered message is silently discarded to make room.<br>\n <code>summarize</code> &mdash; The oldest message is discarded but a truncated preview (first 140 chars) is preserved. When the batch is flushed, these previews are prepended so the agent knows messages were dropped and can see a summary of their content.\n </td></tr>\n <tr><td><strong>Inflight Typing</strong></td><td>When enabled, the typing indicator stays active as long as at least one message is being processed. Without this, a fast command (e.g. <code>/status</code>) finishing while the agent is still working would clear the typing indicator prematurely.</td></tr>\n </tbody>\n </table>\n </div>\n</div>\n<div class="modal-overlay" id="memViewModal">\n <div class="modal sim-modal">\n <button class="close-btn" onclick="closeMemViewModal()">&times;</button>\n <h3 id="memViewTitle">Memory Log</h3>\n <div id="memEditorWrap" class="monaco-wrap readonly" style="height:450px"></div>\n <div class="sim-info">\n <span id="memViewInfo"></span>\n <button class="btn btn-ghost btn-sm" onclick="copyMemView()">Copy</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="simModal">\n <div class="modal sim-modal">\n <button class="close-btn" onclick="closeSimModal()">&times;</button>\n <h3>Simulate Building</h3>\n <div class="sim-tabs">\n <button class="sim-tab active" onclick="switchSimTab(\'main\',this)">Main Agent</button>\n <button class="sim-tab" onclick="switchSimTab(\'subagent\',this)">Subagent</button>\n <button class="sim-tab" onclick="switchSimTab(\'buildinfo\',this)">Build Info</button>\n </div>\n <div id="simEditorWrap" class="monaco-wrap readonly" style="height:450px"></div>\n <div id="simBuildInfo" style="display:none;height:450px;overflow-y:auto;padding:16px;font-size:13px;line-height:1.6"></div>\n <div class="sim-info">\n <span id="simCharCount"></span>\n <button class="btn btn-ghost btn-sm" onclick="copySimPrompt()">Copy</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="memDisableModal">\n <div class="modal">\n <button class="close-btn" onclick="closeMemDisableModal(false)">&times;</button>\n <h3>Disable Memories</h3>\n <p>Disabling memories means the agent will no longer keep track of past conversations. Conversation logs will not be saved and the agent will have no continuity between sessions.</p>\n <p style="font-size:13px;color:var(--text-muted)">Existing memory files will not be deleted.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn btn-danger" onclick="closeMemDisableModal(true)">Disable</button>\n <button class="btn-ghost" onclick="closeMemDisableModal(false)">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="skillDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeSkillDeleteModal()">&times;</button>\n <h3>Delete Skill</h3>\n <p>Are you sure you want to delete <strong id="skillDeleteName"></strong>? This cannot be undone.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteSkill()">Delete</button>\n <button class="btn-ghost" onclick="closeSkillDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="commandDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeCommandDeleteModal()">&times;</button>\n <h3>Delete Command</h3>\n <p>Are you sure you want to delete <strong id="commandDeleteName"></strong>? This cannot be undone.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteCommand()">Delete</button>\n <button class="btn-ghost" onclick="closeCommandDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="pluginDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closePluginDeleteModal()">&times;</button>\n <h3>Delete Plugin</h3>\n <p>Are you sure you want to delete <strong id="pluginDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeletePlugin()">Delete</button>\n <button class="btn-ghost" onclick="closePluginDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="pluginUploadedModal">\n <div class="modal">\n <button class="close-btn" onclick="closePluginUploadedModal()">&times;</button>\n <h3>Plugin Added</h3>\n <p><strong id="pluginUploadedName"></strong> has been uploaded and saved to the configuration.</p>\n <p style="font-size:13px;color:var(--text-muted)">The plugin will appear as <em>invalid</em> until the server is restarted. Restart the server from the Dashboard to activate it.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closePluginUploadedModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="skillEditModal">\n <div class="modal" style="max-width:800px;width:90vw;text-align:left">\n <button class="close-btn" onclick="closeSkillEditModal()">&times;</button>\n <h3 style="margin-bottom:4px">Edit Skill</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:12px"><code id="skillEditPath"></code></p>\n <div id="skillEditorWrap" class="monaco-wrap" style="height:400px"></div>\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px">\n <button class="btn-ghost" onclick="closeSkillEditModal()">Close</button>\n <button class="btn" onclick="saveSkillEdit()">Save</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="commandEditModal">\n <div class="modal" style="max-width:800px;width:90vw;text-align:left">\n <button class="close-btn" onclick="closeCommandEditModal()">&times;</button>\n <h3 style="margin-bottom:4px">Edit Command</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:12px"><code id="commandEditPath"></code></p>\n <div id="commandEditorWrap" class="monaco-wrap" style="height:400px"></div>\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px">\n <button class="btn-ghost" onclick="closeCommandEditModal()">Close</button>\n <button class="btn" onclick="saveCommandEdit()">Save</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="commandUploadedModal">\n <div class="modal">\n <button class="close-btn" onclick="closeCommandUploadedModal()">&times;</button>\n <h3>Command Added</h3>\n <p><strong id="commandUploadedName"></strong> has been uploaded.</p>\n <p style="font-size:13px;color:var(--text-muted)">The command is ready to use immediately.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closeCommandUploadedModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="skillUploadedModal">\n <div class="modal">\n <button class="close-btn" onclick="closeSkillUploadedModal()">&times;</button>\n <h3>Skill Added</h3>\n <p><strong id="skillUploadedName"></strong> has been uploaded.</p>\n <p style="font-size:13px;color:var(--text-muted)">The skill is ready to use immediately.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closeSkillUploadedModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="varDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeVarDeleteModal()">&times;</button>\n <h3>Delete Var</h3>\n <p>Are you sure you want to delete <strong id="varDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteVar()">Delete</button>\n <button class="btn-ghost" onclick="closeVarDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="modelDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeModelDeleteModal()">&times;</button>\n <h3>Delete Model</h3>\n <p>Are you sure you want to delete <strong id="modelDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteModel()">Delete</button>\n <button class="btn-ghost" onclick="closeModelDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="saDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeSaDeleteModal()">&times;</button>\n <h3>Delete Sub Agent</h3>\n <p>Are you sure you want to delete <strong id="saDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteSubAgent()">Delete</button>\n <button class="btn-ghost" onclick="closeSaDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="tokenDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeTokenDeleteModal()">&times;</button>\n <h3>Delete Token</h3>\n <p>Are you sure you want to delete token <strong id="tokenDeleteId"></strong>? This cannot be undone.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteToken()">Delete</button>\n <button class="btn-ghost" onclick="closeTokenDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="regenKeyModal">\n <div class="modal">\n <button class="close-btn" onclick="closeRegenKeyModal()">&times;</button>\n <h3>Regenerate Access Key</h3>\n <p>This will invalidate the current access key. Any saved links or bookmarks using the old key will stop working.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="closeRegenKeyModal();regenKey()">Regenerate</button>\n <button class="btn-ghost" onclick="closeRegenKeyModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="heartbeatSimModal">\n <div class="modal" style="max-width:600px;text-align:left">\n <button class="close-btn" onclick="closeHeartbeatSimModal()">&times;</button>\n <h3 id="heartbeatSimTitle">Heartbeat Simulation</h3>\n <p id="heartbeatSimResult" style="margin:12px 0"></p>\n <label style="font-size:13px;font-weight:600;display:block;margin-bottom:6px">HEARTBEAT.md</label>\n <textarea id="heartbeatSimContent" readonly style="width:100%;height:200px;font-family:var(--mono);font-size:13px;resize:vertical;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px;color:var(--text)"></textarea>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closeHeartbeatSimModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="qrModal">\n <div class="modal">\n <button class="close-btn" onclick="closeQrModal()">&times;</button>\n <h3>Connect WhatsApp</h3>\n <p id="qrMsg">Waiting for QR code...</p>\n <div id="qrSpinner"><span class="spinner"></span> Connecting...</div>\n <img id="qrImg" style="display:none" alt="WhatsApp QR Code">\n <div id="qrStatus"></div>\n <button class="btn btn-ghost" onclick="closeQrModal()" style="margin-top:8px">Close</button>\n </div>\n</div>\n<div class="modal-overlay" id="internalToolsModal">\n <div class="modal" style="max-width:780px;text-align:left;max-height:85vh;display:flex;flex-direction:column">\n <button class="close-btn" onclick="document.getElementById(\'internalToolsModal\').classList.remove(\'open\')">&times;</button>\n <h3 style="margin-bottom:6px">Internal MCP Tools</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">Built-in MCP tool servers registered for the agent.</p>\n <div style="overflow-y:auto;flex:1;min-height:0">\n <div id="internalToolsBody"><span class="spinner"></span> Loading...</div>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="cronMsgModal">\n <div class="modal" style="max-width:680px;text-align:left;max-height:80vh;display:flex;flex-direction:column">\n <button class="close-btn" onclick="document.getElementById(\'cronMsgModal\').classList.remove(\'open\')">&times;</button>\n <h3 id="cronMsgTitle" style="margin-bottom:10px">Job Message</h3>\n <textarea id="cronMsgBody" readonly style="flex:1;min-height:200px;width:100%;resize:none;font-family:monospace;font-size:13px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);padding:12px"></textarea>\n </div>\n</div>\n\x3c!-- Session expired modal --\x3e\n<div class="modal-overlay" id="sessionExpiredModal">\n <div class="modal" style="text-align:center">\n <h3>Session Expired</h3>\n <p style="margin:16px 0">The server was restarted or your session is no longer valid. Please re-authenticate.</p>\n <button class="btn" onclick="location.reload()">OK</button>\n </div>\n</div>\n'}
1
+ export function renderModals(){return'\n\x3c!-- Restart confirmation modal --\x3e\n<div class="modal-overlay" id="restartModal">\n <div class="modal">\n <button class="close-btn" onclick="closeRestartModal()">&times;</button>\n <h3>Restart Server</h3>\n <p>This will restart all channels, agents, and services. Active conversations will be interrupted.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn" onclick="doRestart()">Restart</button>\n <button class="btn-ghost" onclick="closeRestartModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="placeholderRefModal">\n <div class="modal" style="max-width:620px;text-align:left;max-height:85vh;overflow-y:auto">\n <button class="close-btn" onclick="document.getElementById(\'placeholderRefModal\').classList.remove(\'open\')">&times;</button>\n <h3 style="margin-bottom:14px">Placeholder Reference</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">These placeholders are resolved at runtime when building the system prompt from templates.</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:40%">Placeholder</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><code>{{ATTACHMENTS_DIR}}</code></td><td>Path to the session attachments folder, or <code>(memory disabled)</code></td></tr>\n <tr><td><code>{{AVAILABLE_TOOLS}}</code></td><td>Auto-discovered list of all registered MCP tools with name, description, and parameters</td></tr>\n <tr><td><code>{{CHANNEL}}</code></td><td>Channel name, e.g. <code>telegram</code>, <code>responses</code></td></tr>\n <tr><td><code>{{CHAT_ID}}</code></td><td>Chat / conversation ID within the channel</td></tr>\n <tr><td><code>{{DATA_DIR}}</code></td><td>Absolute path to the data directory (workspace files, templates)</td></tr>\n <tr><td><code>{{HEARTBEAT_INSTRUCTIONS}}</code></td><td>Heartbeat/cron behavioral instructions, or empty if cron is disabled</td></tr>\n <tr><td><code>{{HEARTBEAT_PROMPT}}</code></td><td>Raw heartbeat prompt message from config</td></tr>\n <tr><td><code>{{HOSTNAME}}</code></td><td>Server hostname</td></tr>\n <tr><td><code>{{MEMORY_FILE}}</code></td><td>Path to the current conversation memory file, or <code>(memory disabled)</code></td></tr>\n <tr><td><code>{{MESH_INFO}}</code></td><td>Inter-agent mesh communication info. When mesh channel is enabled: agent ID, list of peers, and usage instructions. Empty when mesh is disabled.</td></tr>\n <tr><td><code>{{MESSAGE_TOOLS_INSTRUCTIONS}}</code></td><td>Instructions for cross-channel messaging tools, or empty if not available</td></tr>\n <tr><td><code>{{MODEL}}</code></td><td>Active model ID, e.g. <code>claude-opus-4-6</code></td></tr>\n <tr><td><code>{{NODE_TOOLS_INSTRUCTIONS}}</code></td><td>Instructions for node remote-execution tools, or empty if no nodes connected</td></tr>\n <tr><td><code>{{OS}}</code></td><td>Operating system info (type, release, arch)</td></tr>\n <tr><td><code>{{RUNTIME_LINE}}</code></td><td>Composed runtime info line (host, OS, model, channel, session)</td></tr>\n <tr><td><code>{{SEARCH_IN_MEMORIES}}</code></td><td>Memory search tools instructions (<code>memory_search</code>, <code>memory_get</code>). Populated when Recall Strategy is not <code>builtin-only</code>, empty otherwise.</td></tr>\n <tr><td><code>{{SESSION_ID}}</code></td><td>SDK session ID for resumption, or <code>(new session)</code></td></tr>\n <tr><td><code>{{SESSION_KEY}}</code></td><td>Current session identifier, e.g. <code>telegram:12345</code></td></tr>\n <tr><td><code>{{SUBAGENT_TASK}}</code></td><td>Task description for subagent mode (empty for main agent)</td></tr>\n <tr><td><code>{{TIMEZONE}}</code></td><td>Server timezone, e.g. <code>Europe/Rome</code></td></tr>\n <tr><td><code>{{WORKSPACE_DIR}}</code></td><td>Absolute path to the agent workspace directory</td></tr>\n <tr><td><code>{{WORKSPACE_FILES}}</code></td><td>All workspace .md files inlined as <code>## FileName.md</code> sections</td></tr>\n </tbody>\n </table>\n\n <h4 style="margin:18px 0 8px;font-size:14px">File Includes</h4>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:10px">Workspace files can include other <code>.md</code> files from the data directory using inline placeholders. Includes are resolved once (no recursion).</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:40%">Syntax</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><code>{{FILE:name.md}}</code></td><td>Replaced with the full contents of <code>name.md</code> from the data directory (the same folder where <code>AGENTS.md</code>, <code>SOUL.md</code>, <code>USER.md</code>, etc. live). If the file is not found, a comment <code>&lt;!-- FILE:name.md not found --&gt;</code> is inserted instead. Only <code>[A-Za-z0-9_-]+.md</code> filenames are matched.</td></tr>\n </tbody>\n </table>\n <p style="font-size:12px;color:var(--text-muted);margin-top:6px">Example: place <code>{{FILE:BEHAVIOUR.md}}</code> inside <code>AGENTS.md</code> to inline BEHAVIOUR.md at that position.</p>\n\n <h4 style="margin:18px 0 8px;font-size:14px">Memory Budget (Hot/Cold)</h4>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:10px">When <code>MEMORY.md</code> exceeds 8000 chars, only sections wrapped in hot markers are included in the prompt. Cold content is omitted but remains searchable via <code>memory_search</code>.</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:40%">Marker</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><code>&lt;!-- hot --&gt;</code></td><td>Start of a hot section (included in prompt)</td></tr>\n <tr><td><code>&lt;!-- /hot --&gt;</code></td><td>End of a hot section</td></tr>\n </tbody>\n </table>\n <p style="font-size:12px;color:var(--text-muted);margin-top:6px">If no hot markers are found, the entire file is loaded (backward compatible). Multiple hot sections are supported and concatenated.</p>\n </div>\n</div>\n<div class="modal-overlay" id="agentHelpModal">\n <div class="modal" style="max-width:680px;text-align:left;max-height:85vh;overflow-y:auto">\n <button class="close-btn" onclick="document.getElementById(\'agentHelpModal\').classList.remove(\'open\')">&times;</button>\n <h3 style="margin-bottom:14px">Agent Configuration Reference</h3>\n\n <h4 style="margin:16px 0 8px;font-size:14px;color:var(--accent)">Main</h4>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:30%">Field</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><strong>Default Model</strong></td><td>The model used for all conversations. Must be one of the models defined in the Models section.</td></tr>\n <tr><td><strong>Fallback Model</strong></td><td>If the default model fails (API error, overloaded, etc.), the agent automatically retries with this model. Leave empty to disable fallback.</td></tr>\n </tbody>\n </table>\n\n <h4 style="margin:16px 0 8px;font-size:14px;color:var(--accent)">Configuration</h4>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:30%">Field</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><strong>Max Turns</strong></td><td>Maximum number of agentic turns (tool use round-trips) per single invocation. Prevents runaway loops. A typical value is 5&ndash;15.</td></tr>\n <tr><td><strong>Permission Mode</strong></td><td>Controls how the SDK handles tool permissions.<br><code>default</code> &mdash; ask the user before running tools.<br><code>bypassPermissions</code> &mdash; run all allowed tools without asking (recommended for automated agents).<br><code>plan</code> &mdash; the agent proposes a plan and waits for approval before executing.</td></tr>\n <tr><td><strong>Session TTL</strong></td><td>Time in seconds before an idle session expires. When a session expires, the next message starts a new conversation (no history carried over). Default: 3600 (1 hour).</td></tr>\n <tr><td><strong>Setting Sources</strong></td><td>Which SDK settings files to load.<br><code>nothing</code> &mdash; ignore all settings files.<br><code>user</code> &mdash; load user-level settings (~/.claude).<br><code>project</code> &mdash; load project-level settings from workspace.<br><code>both</code> &mdash; load both user and project settings.</td></tr>\n <tr><td><strong>Coder Skill</strong></td><td>When enabled, the builtin coding prompt is always prepended to the system prompt, adding coding-oriented instructions. When disabled, the system prompt is built entirely from your templates (SYSTEM_PROMPT.md).</td></tr>\n <tr><td><strong>Allowed Tools</strong></td><td>Which SDK tools the agent can use. Unchecked tools are not available to the agent. Common tools: <code>Read</code>, <code>Write</code>, <code>Edit</code> for file operations; <code>Bash</code> for shell commands; <code>Glob</code>/<code>Grep</code> for search; <code>WebSearch</code>/<code>WebFetch</code> for web access; <code>Task</code> for sub-agents; <code>Skill</code> for slash commands.</td></tr>\n <tr><td><strong>Auto Approve Tools</strong></td><td>Controls how tool permission requests from the SDK are handled.<br><strong>On (default)</strong> &mdash; all tool invocations are automatically approved. This prevents the SDK from blocking when <code>permissionMode</code> is set to <code>default</code>.<br><strong>Off</strong> &mdash; tool permission requests are forwarded to the user&rsquo;s channel as interactive messages with Approve/Deny buttons. The SDK waits until the user responds before proceeding.</td></tr>\n </tbody>\n </table>\n\n <h4 style="margin:16px 0 8px;font-size:14px;color:var(--accent)">Message Queue</h4>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:10px">Controls how incoming messages are handled while the agent is already processing a response.</p>\n <table class="tbl" style="font-size:13px">\n <thead><tr><th style="width:30%">Field</th><th>Description</th></tr></thead>\n <tbody>\n <tr><td><strong>Queue Mode</strong></td><td>\n <code>queue</code> &mdash; Simple FIFO. Each message is processed one at a time in order. The user must wait for each response before the next message is handled.<br>\n <code>collect</code> &mdash; Messages sent while the agent is busy are buffered. After a debounce period, all buffered messages are merged into a single prompt and processed together.<br>\n <code>steer</code> &mdash; A new message interrupts the current processing. The agent stops its current response and immediately starts handling the new message, combining context from both.\n </td></tr>\n <tr><td><strong>Debounce (ms)</strong></td><td>Only applies in <code>collect</code> mode. After the last buffered message arrives, the system waits this many milliseconds before flushing the batch. This allows grouping rapid-fire messages into a single prompt. Set to 0 for immediate flush.</td></tr>\n <tr><td><strong>Queue Cap</strong></td><td>Maximum number of messages that can accumulate in the buffer. When this limit is reached, the Drop Policy kicks in. Set to 0 for unlimited.</td></tr>\n <tr><td><strong>Drop Policy</strong></td><td>What happens when Queue Cap is exceeded:<br>\n <code>new</code> &mdash; The incoming message is rejected; the user receives an error asking to wait.<br>\n <code>old</code> &mdash; The oldest buffered message is silently discarded to make room.<br>\n <code>summarize</code> &mdash; The oldest message is discarded but a truncated preview (first 140 chars) is preserved. When the batch is flushed, these previews are prepended so the agent knows messages were dropped and can see a summary of their content.\n </td></tr>\n <tr><td><strong>Inflight Typing</strong></td><td>When enabled, the typing indicator stays active as long as at least one message is being processed. Without this, a fast command (e.g. <code>/status</code>) finishing while the agent is still working would clear the typing indicator prematurely.</td></tr>\n </tbody>\n </table>\n </div>\n</div>\n<div class="modal-overlay" id="memViewModal">\n <div class="modal sim-modal">\n <button class="close-btn" onclick="closeMemViewModal()">&times;</button>\n <h3 id="memViewTitle">Memory Log</h3>\n <div id="memEditorWrap" class="monaco-wrap readonly" style="height:450px"></div>\n <div class="sim-info">\n <span id="memViewInfo"></span>\n <button class="btn btn-ghost btn-sm" onclick="copyMemView()">Copy</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="simModal">\n <div class="modal sim-modal">\n <button class="close-btn" onclick="closeSimModal()">&times;</button>\n <h3>Simulate Building</h3>\n <div class="sim-tabs">\n <button class="sim-tab active" onclick="switchSimTab(\'main\',this)">Main Agent</button>\n <button class="sim-tab" onclick="switchSimTab(\'subagent\',this)">Subagent</button>\n <button class="sim-tab" onclick="switchSimTab(\'buildinfo\',this)">Build Info</button>\n </div>\n <div id="simEditorWrap" class="monaco-wrap readonly" style="height:450px"></div>\n <div id="simBuildInfo" style="display:none;height:450px;overflow-y:auto;padding:16px;font-size:13px;line-height:1.6"></div>\n <div class="sim-info">\n <span id="simCharCount"></span>\n <button class="btn btn-ghost btn-sm" onclick="copySimPrompt()">Copy</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="memDisableModal">\n <div class="modal">\n <button class="close-btn" onclick="closeMemDisableModal(false)">&times;</button>\n <h3>Disable Memories</h3>\n <p>Disabling memories means the agent will no longer keep track of past conversations. Conversation logs will not be saved and the agent will have no continuity between sessions.</p>\n <p style="font-size:13px;color:var(--text-muted)">Existing memory files will not be deleted.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn btn-danger" onclick="closeMemDisableModal(true)">Disable</button>\n <button class="btn-ghost" onclick="closeMemDisableModal(false)">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="skillDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeSkillDeleteModal()">&times;</button>\n <h3>Delete Skill</h3>\n <p>Are you sure you want to delete <strong id="skillDeleteName"></strong>? This cannot be undone.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteSkill()">Delete</button>\n <button class="btn-ghost" onclick="closeSkillDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="commandDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeCommandDeleteModal()">&times;</button>\n <h3>Delete Command</h3>\n <p>Are you sure you want to delete <strong id="commandDeleteName"></strong>? This cannot be undone.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteCommand()">Delete</button>\n <button class="btn-ghost" onclick="closeCommandDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="pluginDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closePluginDeleteModal()">&times;</button>\n <h3>Delete Plugin</h3>\n <p>Are you sure you want to delete <strong id="pluginDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeletePlugin()">Delete</button>\n <button class="btn-ghost" onclick="closePluginDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="pluginUploadedModal">\n <div class="modal">\n <button class="close-btn" onclick="closePluginUploadedModal()">&times;</button>\n <h3>Plugin Added</h3>\n <p><strong id="pluginUploadedName"></strong> has been uploaded and saved to the configuration.</p>\n <p style="font-size:13px;color:var(--text-muted)">The plugin will appear as <em>invalid</em> until the server is restarted. Restart the server from the Dashboard to activate it.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closePluginUploadedModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="skillEditModal">\n <div class="modal" style="max-width:800px;width:90vw;text-align:left">\n <button class="close-btn" onclick="closeSkillEditModal()">&times;</button>\n <h3 style="margin-bottom:4px">Edit Skill</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:12px"><code id="skillEditPath"></code></p>\n <div id="skillEditorWrap" class="monaco-wrap" style="height:400px"></div>\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px">\n <button class="btn-ghost" onclick="closeSkillEditModal()">Close</button>\n <button class="btn" onclick="saveSkillEdit()">Save</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="commandEditModal">\n <div class="modal" style="max-width:800px;width:90vw;text-align:left">\n <button class="close-btn" onclick="closeCommandEditModal()">&times;</button>\n <h3 style="margin-bottom:4px">Edit Command</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:12px"><code id="commandEditPath"></code></p>\n <div id="commandEditorWrap" class="monaco-wrap" style="height:400px"></div>\n <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px">\n <button class="btn-ghost" onclick="closeCommandEditModal()">Close</button>\n <button class="btn" onclick="saveCommandEdit()">Save</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="commandUploadedModal">\n <div class="modal">\n <button class="close-btn" onclick="closeCommandUploadedModal()">&times;</button>\n <h3>Command Added</h3>\n <p><strong id="commandUploadedName"></strong> has been uploaded.</p>\n <p style="font-size:13px;color:var(--text-muted)">The command is ready to use immediately.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closeCommandUploadedModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="skillUploadedModal">\n <div class="modal">\n <button class="close-btn" onclick="closeSkillUploadedModal()">&times;</button>\n <h3>Skill Added</h3>\n <p><strong id="skillUploadedName"></strong> has been uploaded.</p>\n <p style="font-size:13px;color:var(--text-muted)">The skill is ready to use immediately.</p>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closeSkillUploadedModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="varDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeVarDeleteModal()">&times;</button>\n <h3>Delete Var</h3>\n <p>Are you sure you want to delete <strong id="varDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteVar()">Delete</button>\n <button class="btn-ghost" onclick="closeVarDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="modelDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeModelDeleteModal()">&times;</button>\n <h3>Delete Model</h3>\n <p>Are you sure you want to delete <strong id="modelDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteModel()">Delete</button>\n <button class="btn-ghost" onclick="closeModelDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="saDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeSaDeleteModal()">&times;</button>\n <h3>Delete Sub Agent</h3>\n <p>Are you sure you want to delete <strong id="saDeleteName"></strong>?</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteSubAgent()">Delete</button>\n <button class="btn-ghost" onclick="closeSaDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="tokenDeleteModal">\n <div class="modal">\n <button class="close-btn" onclick="closeTokenDeleteModal()">&times;</button>\n <h3>Delete Token</h3>\n <p>Are you sure you want to delete token <strong id="tokenDeleteId"></strong>? This cannot be undone.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="doDeleteToken()">Delete</button>\n <button class="btn-ghost" onclick="closeTokenDeleteModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="regenKeyModal">\n <div class="modal">\n <button class="close-btn" onclick="closeRegenKeyModal()">&times;</button>\n <h3>Regenerate Access Key</h3>\n <p>This will invalidate the current access key. Any saved links or bookmarks using the old key will stop working.</p>\n <div style="display:flex;gap:8px;justify-content:center">\n <button class="btn btn-danger" onclick="closeRegenKeyModal();regenKey()">Regenerate</button>\n <button class="btn-ghost" onclick="closeRegenKeyModal()">Cancel</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="heartbeatSimModal">\n <div class="modal" style="max-width:600px;text-align:left">\n <button class="close-btn" onclick="closeHeartbeatSimModal()">&times;</button>\n <h3 id="heartbeatSimTitle">Heartbeat Simulation</h3>\n <p id="heartbeatSimResult" style="margin:12px 0"></p>\n <label style="font-size:13px;font-weight:600;display:block;margin-bottom:6px">HEARTBEAT.md</label>\n <textarea id="heartbeatSimContent" readonly style="width:100%;height:200px;font-family:var(--mono);font-size:13px;resize:vertical;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px;color:var(--text)"></textarea>\n <div style="display:flex;gap:8px;justify-content:center;margin-top:16px">\n <button class="btn" onclick="closeHeartbeatSimModal()">OK</button>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="qrModal">\n <div class="modal">\n <button class="close-btn" onclick="closeQrModal()">&times;</button>\n <h3>Connect WhatsApp</h3>\n <p id="qrMsg">Waiting for QR code...</p>\n <div id="qrSpinner"><span class="spinner"></span> Connecting...</div>\n <img id="qrImg" style="display:none" alt="WhatsApp QR Code">\n <div id="qrStatus"></div>\n <button class="btn btn-ghost" onclick="closeQrModal()" style="margin-top:8px">Close</button>\n </div>\n</div>\n<div class="modal-overlay" id="internalToolsModal">\n <div class="modal" style="max-width:780px;text-align:left;max-height:85vh;display:flex;flex-direction:column">\n <button class="close-btn" onclick="document.getElementById(\'internalToolsModal\').classList.remove(\'open\')">&times;</button>\n <h3 style="margin-bottom:6px">Internal MCP Tools</h3>\n <p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">Built-in MCP tool servers registered for the agent.</p>\n <div style="overflow-y:auto;flex:1;min-height:0">\n <div id="internalToolsBody"><span class="spinner"></span> Loading...</div>\n </div>\n </div>\n</div>\n<div class="modal-overlay" id="cronMsgModal">\n <div class="modal" style="max-width:680px;text-align:left;max-height:80vh;display:flex;flex-direction:column">\n <button class="close-btn" onclick="document.getElementById(\'cronMsgModal\').classList.remove(\'open\')">&times;</button>\n <h3 id="cronMsgTitle" style="margin-bottom:10px">Job Message</h3>\n <textarea id="cronMsgBody" readonly style="flex:1;min-height:200px;width:100%;resize:none;font-family:monospace;font-size:13px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);padding:12px"></textarea>\n </div>\n</div>\n\x3c!-- Session expired modal --\x3e\n<div class="modal-overlay" id="sessionExpiredModal">\n <div class="modal" style="text-align:center">\n <h3>Session Expired</h3>\n <p style="margin:16px 0">The server was restarted or your session is no longer valid. Please re-authenticate.</p>\n <button class="btn" onclick="location.reload()">OK</button>\n </div>\n</div>\n'}
@@ -1 +1 @@
1
- export function channelsJS(){return"\n/* ---- Channels ---- */\nconst CHANNEL_LIST = ['telegram','responses','whatsapp','discord','slack','signal','msteams','googlechat','line','matrix'];\nconst SUPPORTED_CHANNELS = ['telegram','responses','whatsapp'];\n\nconst CHANNEL_HELP = {\n telegram: {\n title: 'Telegram Setup',\n steps: [\n 'Open Telegram and message <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>',\n 'Send <code>/newbot</code>',\n 'Choose a display name, then a username (must end in <code>bot</code>)',\n 'BotFather replies with a <b>bot token</b> &mdash; paste it below',\n 'Optional: send <code>/setprivacy</code> &rarr; Disable, so the bot can see all group messages',\n '<b>DM Policy</b> controls who can talk to the bot: <b>open</b> = anyone can message, <b>allowlist</b> = only Telegram user IDs listed in Allow From can message',\n ]\n },\n discord: {\n title: 'Discord Setup',\n steps: [\n 'Go to <a href=\"https://discord.com/developers/applications\" target=\"_blank\">Discord Developer Portal</a> &rarr; <b>New Application</b>',\n '<b>Bot</b> tab &rarr; click <b>Reset Token</b> &rarr; copy the token and paste it below',\n 'On the same page, enable <b>Message Content Intent</b> under Privileged Gateway Intents',\n '<b>OAuth2 &rarr; URL Generator</b> &rarr; scope: <code>bot</code>, permission: <code>Send Messages</code>',\n 'Open the generated URL to invite the bot to your server',\n ]\n },\n slack: {\n title: 'Slack Setup',\n steps: [\n 'Go to <a href=\"https://api.slack.com/apps\" target=\"_blank\">api.slack.com/apps</a> &rarr; <b>Create New App</b> &rarr; From scratch',\n '<b>Socket Mode</b> &rarr; enable &rarr; generate an App-Level Token with scope <code>connections:write</code> &rarr; copy it (this is the <b>App Token</b>, starts with <code>xapp-</code>)',\n '<b>OAuth &amp; Permissions</b> &rarr; add Bot Token Scopes: <code>chat:write</code>, <code>im:history</code>, <code>im:read</code>',\n '<b>Event Subscriptions</b> &rarr; enable &rarr; subscribe to bot event <code>message.im</code>',\n 'Install the app to your workspace &rarr; copy the <b>Bot Token</b> (starts with <code>xoxb-</code>)',\n ]\n },\n whatsapp: {\n title: 'WhatsApp Setup',\n steps: [\n 'Set the <b>Auth Directory</b> below to a local folder (e.g. <code>./data/whatsapp</code>)',\n 'Enable the channel, save, then click <b>Connect WhatsApp</b> &mdash; a QR code will appear in a pop-up',\n 'Open WhatsApp on your phone &rarr; <b>Settings</b> &rarr; <b>Linked Devices</b> &rarr; scan the QR code',\n 'Once scanned, the pop-up confirms the connection. The session persists in the auth directory',\n '<b>DM Policy</b>: <b>open</b> = anyone can message, <b>allowlist</b> = only the phone numbers listed in Allow From can message (E.164 format, e.g. <code>+393331234567</code>)',\n ]\n },\n signal: {\n title: 'Signal Setup',\n steps: [\n 'Install <a href=\"https://github.com/bbernhard/signal-cli-rest-api\" target=\"_blank\">signal-cli-rest-api</a> and start it',\n 'Register or link a phone number via the signal-cli REST API',\n 'Enter the <b>API URL</b> (e.g. <code>http://localhost:8080</code>) and <b>Phone Number</b> below',\n ]\n },\n msteams: {\n title: 'Microsoft Teams Setup',\n steps: [\n 'Go to <a href=\"https://dev.teams.microsoft.com/\" target=\"_blank\">Teams Developer Portal</a> &rarr; <b>Apps</b> &rarr; New App',\n 'Under <b>App Features</b> &rarr; add a <b>Bot</b>',\n 'In Azure, register a new <b>Bot Channel Registration</b> &rarr; copy <b>App ID</b> and <b>App Secret</b>',\n 'Set the messaging endpoint to your server URL',\n 'Paste App ID and App Secret below',\n ]\n },\n googlechat: {\n title: 'Google Chat Setup',\n steps: [\n 'Go to <a href=\"https://console.cloud.google.com/\" target=\"_blank\">Google Cloud Console</a> &rarr; create or select a project',\n 'Enable the <b>Google Chat API</b>',\n '<b>Configuration</b> tab &rarr; set Bot URL to your server endpoint',\n 'Create a <b>Service Account</b> &rarr; download the JSON key file',\n 'Enter the path to the <b>credentials file</b> and <b>space ID</b> below',\n ]\n },\n line: {\n title: 'LINE Setup',\n steps: [\n 'Go to <a href=\"https://developers.line.biz/console/\" target=\"_blank\">LINE Developers Console</a> &rarr; create a Provider &rarr; create a <b>Messaging API</b> channel',\n 'Under <b>Messaging API</b> tab, issue a <b>Channel Access Token</b> &rarr; paste it below',\n 'Copy the <b>Channel Secret</b> from the <b>Basic Settings</b> tab',\n 'Set the <b>Webhook URL</b> to your server endpoint and enable <b>Use Webhook</b>',\n ]\n },\n matrix: {\n title: 'Matrix Setup',\n steps: [\n 'Create a bot account on your Matrix homeserver (e.g. via <code>register_new_matrix_user</code>)',\n 'Enter the <b>Homeserver URL</b> (e.g. <code>https://matrix.example.com</code>)',\n 'Enter the bot <b>User ID</b> (e.g. <code>@bot:example.com</code>) and <b>Access Token</b>',\n 'Invite the bot to the rooms you want it to participate in',\n ]\n },\n responses: {\n title: 'Responses API',\n steps: [\n 'This is a built-in HTTP API compatible with the OpenAI Responses format',\n 'Create an API token in the <b>Tokens</b> section to authenticate requests',\n 'Send requests to <code>POST http://&lt;host&gt;:&lt;port&gt;/v1/responses</code> with <code>Authorization: Bearer &lt;token&gt;</code>',\n ]\n },\n};\n\nfunction channelFields(ch, cfg) {\n const accts = cfg.accounts || {};\n const dfl = Object.values(accts)[0] || {};\n const aid = Object.keys(accts)[0] || 'default';\n let h = '';\n switch (ch) {\n case 'telegram':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n var dp = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dp==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dp==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(Telegram user IDs, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n break;\n case 'discord':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.token\" value=\"'+esc(dfl.token||'')+'\"></div>';\n break;\n case 'slack':\n h += '<div class=\"field\"><label>Bot Token <span style=\"color:var(--text-muted);font-weight:400\">(xoxb-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Token <span style=\"color:var(--text-muted);font-weight:400\">(xapp-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appToken\" value=\"'+esc(dfl.appToken||'')+'\"></div>';\n break;\n case 'whatsapp':\n h += '<div class=\"field\"><label>Auth Directory</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.authDir\" value=\"'+esc(dfl.authDir||'./data/whatsapp')+'\"></div>';\n var dpw = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dpw==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dpw==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(phone numbers in E.164 format, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n h += '<div class=\"field\"><button type=\"button\" class=\"btn btn-sm\" onclick=\"openWhatsAppQr()\" id=\"wa-connect-btn\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px\"><path d=\"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4\"/><polyline points=\"10 17 15 12 10 7\"/><line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\"/></svg>Connect WhatsApp</button></div>';\n break;\n case 'signal':\n h += '<div class=\"field\"><label>API URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.apiUrl\" value=\"'+esc(dfl.apiUrl||'http://localhost:8080')+'\"></div>';\n h += '<div class=\"field\"><label>Phone Number</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.phoneNumber\" value=\"'+esc(dfl.phoneNumber||'')+'\"></div>';\n break;\n case 'msteams':\n h += '<div class=\"field\"><label>App ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appId\" value=\"'+esc(dfl.appId||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Secret</label><input type=\"password\" data-ch-field=\"'+ch+'.'+aid+'.appSecret\" value=\"'+esc(dfl.appSecret||'')+'\"></div>';\n break;\n case 'googlechat':\n h += '<div class=\"field\"><label>Credentials File</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.credentialsFile\" value=\"'+esc(dfl.credentialsFile||'')+'\"></div>';\n h += '<div class=\"field\"><label>Space ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.spaceId\" value=\"'+esc(dfl.spaceId||'')+'\"></div>';\n break;\n case 'line':\n h += '<div class=\"field\"><label>Channel Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelAccessToken\" value=\"'+esc(dfl.channelAccessToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>Channel Secret</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelSecret\" value=\"'+esc(dfl.channelSecret||'')+'\"></div>';\n break;\n case 'matrix':\n h += '<div class=\"field\"><label>Homeserver URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.homeserverUrl\" value=\"'+esc(dfl.homeserverUrl||'')+'\"></div>';\n h += '<div class=\"field\"><label>User ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.userId\" value=\"'+esc(dfl.userId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.accessToken\" value=\"'+esc(dfl.accessToken||'')+'\"></div>';\n break;\n case 'responses':\n h += '<div class=\"field\"><label>Port</label><input type=\"number\" data-ch-field=\"'+ch+'.port\" value=\"'+(cfg.port||3000)+'\"></div>';\n break;\n }\n return h;\n}\n\nasync function loadChannels(){\n currentConfig = await fetchAPI('/config');\n const wrap = document.getElementById('channelCards');\n wrap.innerHTML='';\n for(const ch of CHANNEL_LIST){\n const cfg = currentConfig.channels?.[ch]||{enabled:false};\n const help = CHANNEL_HELP[ch];\n const helpId = 'help-'+ch;\n const supported = SUPPORTED_CHANNELS.indexOf(ch) !== -1;\n if(!supported){ continue; }\n let html = '<div class=\"card\" data-channel=\"'+ch+'\">';\n html += '<div class=\"card-header\"><span class=\"card-title\" style=\"text-transform:capitalize\">'+esc(ch)+'</span>';\n html += '<label class=\"toggle\"><input type=\"checkbox\" data-ch-toggle=\"'+ch+'\" '+(cfg.enabled?'checked':'')+'><span></span></label></div>';\n html += '<div class=\"ch-fields\" data-ch-fields=\"'+ch+'\" style=\"'+(cfg.enabled?'':'display:none')+'\">';\n if(help){\n html += '<div style=\"margin-bottom:10px\"><button class=\"help-toggle\" type=\"button\" onclick=\"toggleHelp(this)\" data-help=\"'+helpId+'\" title=\"Setup guide\">?</button> <span style=\"font-size:13px;color:var(--text-muted)\">How to set up</span></div>';\n html += '<div class=\"help-panel\" id=\"'+helpId+'\"><b>'+help.title+'</b><ol>';\n for(const s of help.steps) html += '<li>'+s+'</li>';\n html += '</ol></div>';\n }\n html += channelFields(ch, cfg);\n html += '</div></div>';\n wrap.innerHTML += html;\n }\n wrap.querySelectorAll('[data-ch-toggle]').forEach(inp=>{\n inp.addEventListener('change',e=>{\n const fields = wrap.querySelector('[data-ch-fields=\"'+e.target.dataset.chToggle+'\"]');\n if(fields) fields.style.display = e.target.checked ? '' : 'none';\n });\n });\n markEnvRefs(wrap);\n}\n"}
1
+ export function channelsJS(){return"\n/* ---- Channels ---- */\nconst CHANNEL_LIST = ['telegram','responses','mesh','whatsapp','discord','slack','signal','msteams','googlechat','line','matrix'];\nconst SUPPORTED_CHANNELS = ['telegram','responses','mesh','whatsapp'];\n\nconst CHANNEL_HELP = {\n telegram: {\n title: 'Telegram Setup',\n steps: [\n 'Open Telegram and message <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>',\n 'Send <code>/newbot</code>',\n 'Choose a display name, then a username (must end in <code>bot</code>)',\n 'BotFather replies with a <b>bot token</b> &mdash; paste it below',\n 'Optional: send <code>/setprivacy</code> &rarr; Disable, so the bot can see all group messages',\n '<b>DM Policy</b> controls who can talk to the bot: <b>open</b> = anyone can message, <b>allowlist</b> = only Telegram user IDs listed in Allow From can message',\n ]\n },\n discord: {\n title: 'Discord Setup',\n steps: [\n 'Go to <a href=\"https://discord.com/developers/applications\" target=\"_blank\">Discord Developer Portal</a> &rarr; <b>New Application</b>',\n '<b>Bot</b> tab &rarr; click <b>Reset Token</b> &rarr; copy the token and paste it below',\n 'On the same page, enable <b>Message Content Intent</b> under Privileged Gateway Intents',\n '<b>OAuth2 &rarr; URL Generator</b> &rarr; scope: <code>bot</code>, permission: <code>Send Messages</code>',\n 'Open the generated URL to invite the bot to your server',\n ]\n },\n slack: {\n title: 'Slack Setup',\n steps: [\n 'Go to <a href=\"https://api.slack.com/apps\" target=\"_blank\">api.slack.com/apps</a> &rarr; <b>Create New App</b> &rarr; From scratch',\n '<b>Socket Mode</b> &rarr; enable &rarr; generate an App-Level Token with scope <code>connections:write</code> &rarr; copy it (this is the <b>App Token</b>, starts with <code>xapp-</code>)',\n '<b>OAuth &amp; Permissions</b> &rarr; add Bot Token Scopes: <code>chat:write</code>, <code>im:history</code>, <code>im:read</code>',\n '<b>Event Subscriptions</b> &rarr; enable &rarr; subscribe to bot event <code>message.im</code>',\n 'Install the app to your workspace &rarr; copy the <b>Bot Token</b> (starts with <code>xoxb-</code>)',\n ]\n },\n whatsapp: {\n title: 'WhatsApp Setup',\n steps: [\n 'Set the <b>Auth Directory</b> below to a local folder (e.g. <code>./data/whatsapp</code>)',\n 'Enable the channel, save, then click <b>Connect WhatsApp</b> &mdash; a QR code will appear in a pop-up',\n 'Open WhatsApp on your phone &rarr; <b>Settings</b> &rarr; <b>Linked Devices</b> &rarr; scan the QR code',\n 'Once scanned, the pop-up confirms the connection. The session persists in the auth directory',\n '<b>DM Policy</b>: <b>open</b> = anyone can message, <b>allowlist</b> = only the phone numbers listed in Allow From can message (E.164 format, e.g. <code>+393331234567</code>)',\n ]\n },\n signal: {\n title: 'Signal Setup',\n steps: [\n 'Install <a href=\"https://github.com/bbernhard/signal-cli-rest-api\" target=\"_blank\">signal-cli-rest-api</a> and start it',\n 'Register or link a phone number via the signal-cli REST API',\n 'Enter the <b>API URL</b> (e.g. <code>http://localhost:8080</code>) and <b>Phone Number</b> below',\n ]\n },\n msteams: {\n title: 'Microsoft Teams Setup',\n steps: [\n 'Go to <a href=\"https://dev.teams.microsoft.com/\" target=\"_blank\">Teams Developer Portal</a> &rarr; <b>Apps</b> &rarr; New App',\n 'Under <b>App Features</b> &rarr; add a <b>Bot</b>',\n 'In Azure, register a new <b>Bot Channel Registration</b> &rarr; copy <b>App ID</b> and <b>App Secret</b>',\n 'Set the messaging endpoint to your server URL',\n 'Paste App ID and App Secret below',\n ]\n },\n googlechat: {\n title: 'Google Chat Setup',\n steps: [\n 'Go to <a href=\"https://console.cloud.google.com/\" target=\"_blank\">Google Cloud Console</a> &rarr; create or select a project',\n 'Enable the <b>Google Chat API</b>',\n '<b>Configuration</b> tab &rarr; set Bot URL to your server endpoint',\n 'Create a <b>Service Account</b> &rarr; download the JSON key file',\n 'Enter the path to the <b>credentials file</b> and <b>space ID</b> below',\n ]\n },\n line: {\n title: 'LINE Setup',\n steps: [\n 'Go to <a href=\"https://developers.line.biz/console/\" target=\"_blank\">LINE Developers Console</a> &rarr; create a Provider &rarr; create a <b>Messaging API</b> channel',\n 'Under <b>Messaging API</b> tab, issue a <b>Channel Access Token</b> &rarr; paste it below',\n 'Copy the <b>Channel Secret</b> from the <b>Basic Settings</b> tab',\n 'Set the <b>Webhook URL</b> to your server endpoint and enable <b>Use Webhook</b>',\n ]\n },\n matrix: {\n title: 'Matrix Setup',\n steps: [\n 'Create a bot account on your Matrix homeserver (e.g. via <code>register_new_matrix_user</code>)',\n 'Enter the <b>Homeserver URL</b> (e.g. <code>https://matrix.example.com</code>)',\n 'Enter the bot <b>User ID</b> (e.g. <code>@bot:example.com</code>) and <b>Access Token</b>',\n 'Invite the bot to the rooms you want it to participate in',\n ]\n },\n responses: {\n title: 'Responses API',\n steps: [\n 'This is a built-in HTTP API compatible with the OpenAI Responses format',\n 'Create an API token in the <b>Tokens</b> section to authenticate requests',\n 'Send requests to <code>POST http://&lt;host&gt;:&lt;port&gt;/v1/responses</code> with <code>Authorization: Bearer &lt;token&gt;</code>',\n ]\n },\n mesh: {\n title: 'Mesh (Inter-Agent Messaging)',\n steps: [\n 'Mesh enables direct agent-to-agent communication via a local WebSocket broker',\n 'Start the <b>hera-mesh</b> broker (<code>dev-start.sh</code> handles this automatically)',\n 'Set <b>Agent ID</b> to this agent\\'s name (e.g. <code>dante</code>, <code>beatrice</code>)',\n 'Set <b>Private Key</b> using an env var reference (e.g. <code>$\\{MESH_DANTE_KEY}</code>)',\n 'Add the env var in <b>Engine &rarr; Vars</b> with the Ed25519 private key (hex)',\n 'The agent must be registered in the broker\\'s <code>config/agents.yaml</code> with matching public key',\n ]\n },\n};\n\nfunction channelFields(ch, cfg) {\n const accts = cfg.accounts || {};\n const dfl = Object.values(accts)[0] || {};\n const aid = Object.keys(accts)[0] || 'default';\n let h = '';\n switch (ch) {\n case 'telegram':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n var dp = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dp==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dp==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(Telegram user IDs, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n break;\n case 'discord':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.token\" value=\"'+esc(dfl.token||'')+'\"></div>';\n break;\n case 'slack':\n h += '<div class=\"field\"><label>Bot Token <span style=\"color:var(--text-muted);font-weight:400\">(xoxb-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Token <span style=\"color:var(--text-muted);font-weight:400\">(xapp-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appToken\" value=\"'+esc(dfl.appToken||'')+'\"></div>';\n break;\n case 'whatsapp':\n h += '<div class=\"field\"><label>Auth Directory</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.authDir\" value=\"'+esc(dfl.authDir||'./data/whatsapp')+'\"></div>';\n var dpw = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dpw==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dpw==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(phone numbers in E.164 format, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n h += '<div class=\"field\"><button type=\"button\" class=\"btn btn-sm\" onclick=\"openWhatsAppQr()\" id=\"wa-connect-btn\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px\"><path d=\"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4\"/><polyline points=\"10 17 15 12 10 7\"/><line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\"/></svg>Connect WhatsApp</button></div>';\n break;\n case 'signal':\n h += '<div class=\"field\"><label>API URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.apiUrl\" value=\"'+esc(dfl.apiUrl||'http://localhost:8080')+'\"></div>';\n h += '<div class=\"field\"><label>Phone Number</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.phoneNumber\" value=\"'+esc(dfl.phoneNumber||'')+'\"></div>';\n break;\n case 'msteams':\n h += '<div class=\"field\"><label>App ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appId\" value=\"'+esc(dfl.appId||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Secret</label><input type=\"password\" data-ch-field=\"'+ch+'.'+aid+'.appSecret\" value=\"'+esc(dfl.appSecret||'')+'\"></div>';\n break;\n case 'googlechat':\n h += '<div class=\"field\"><label>Credentials File</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.credentialsFile\" value=\"'+esc(dfl.credentialsFile||'')+'\"></div>';\n h += '<div class=\"field\"><label>Space ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.spaceId\" value=\"'+esc(dfl.spaceId||'')+'\"></div>';\n break;\n case 'line':\n h += '<div class=\"field\"><label>Channel Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelAccessToken\" value=\"'+esc(dfl.channelAccessToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>Channel Secret</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelSecret\" value=\"'+esc(dfl.channelSecret||'')+'\"></div>';\n break;\n case 'matrix':\n h += '<div class=\"field\"><label>Homeserver URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.homeserverUrl\" value=\"'+esc(dfl.homeserverUrl||'')+'\"></div>';\n h += '<div class=\"field\"><label>User ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.userId\" value=\"'+esc(dfl.userId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.accessToken\" value=\"'+esc(dfl.accessToken||'')+'\"></div>';\n break;\n case 'responses':\n h += '<div class=\"field\"><label>Port</label><input type=\"number\" data-ch-field=\"'+ch+'.port\" value=\"'+(cfg.port||3000)+'\"></div>';\n break;\n case 'mesh':\n h += '<div class=\"field\"><label>Broker URL</label><input type=\"text\" data-ch-field=\"'+ch+'.brokerUrl\" value=\"'+esc(cfg.brokerUrl||'ws://127.0.0.1:3780/ws')+'\"></div>';\n h += '<div class=\"field\"><label>Agent ID</label><input type=\"text\" data-ch-field=\"'+ch+'.agentId\" value=\"'+esc(cfg.agentId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Private Key <span style=\"color:var(--text-muted);font-weight:400\">(use env var: <code>$\\{MESH_KEY}</code>)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.privateKey\" value=\"'+esc(cfg.privateKey||'')+'\"></div>';\n break;\n }\n return h;\n}\n\nasync function loadChannels(){\n currentConfig = await fetchAPI('/config');\n const wrap = document.getElementById('channelCards');\n wrap.innerHTML='';\n for(const ch of CHANNEL_LIST){\n const cfg = currentConfig.channels?.[ch]||{enabled:false};\n const help = CHANNEL_HELP[ch];\n const helpId = 'help-'+ch;\n const supported = SUPPORTED_CHANNELS.indexOf(ch) !== -1;\n if(!supported){ continue; }\n let html = '<div class=\"card\" data-channel=\"'+ch+'\">';\n html += '<div class=\"card-header\"><span class=\"card-title\" style=\"text-transform:capitalize\">'+esc(ch)+'</span>';\n html += '<label class=\"toggle\"><input type=\"checkbox\" data-ch-toggle=\"'+ch+'\" '+(cfg.enabled?'checked':'')+'><span></span></label></div>';\n html += '<div class=\"ch-fields\" data-ch-fields=\"'+ch+'\" style=\"'+(cfg.enabled?'':'display:none')+'\">';\n if(help){\n html += '<div style=\"margin-bottom:10px\"><button class=\"help-toggle\" type=\"button\" onclick=\"toggleHelp(this)\" data-help=\"'+helpId+'\" title=\"Setup guide\">?</button> <span style=\"font-size:13px;color:var(--text-muted)\">How to set up</span></div>';\n html += '<div class=\"help-panel\" id=\"'+helpId+'\"><b>'+help.title+'</b><ol>';\n for(const s of help.steps) html += '<li>'+s+'</li>';\n html += '</ol></div>';\n }\n html += channelFields(ch, cfg);\n html += '</div></div>';\n wrap.innerHTML += html;\n }\n wrap.querySelectorAll('[data-ch-toggle]').forEach(inp=>{\n inp.addEventListener('change',e=>{\n const fields = wrap.querySelector('[data-ch-fields=\"'+e.target.dataset.chToggle+'\"]');\n if(fields) fields.style.display = e.target.checked ? '' : 'none';\n });\n });\n markEnvRefs(wrap);\n}\n"}
package/dist/server.d.ts CHANGED
@@ -4,6 +4,7 @@ import { NodeSignatureDB } from "./auth/node-signature-db.js";
4
4
  import { SessionDB } from "./agent/session-db.js";
5
5
  import { ChannelManager } from "./gateway/channel-manager.js";
6
6
  import { WebChatChannel } from "./gateway/channels/webchat.js";
7
+ import { MeshChannel } from "./gateway/channels/mesh.js";
7
8
  import { AgentService } from "./agent/agent-service.js";
8
9
  import { NodeRegistry } from "./gateway/node-registry.js";
9
10
  import { MemoryManager } from "./memory/memory-manager.js";
@@ -35,6 +36,7 @@ export declare class Server {
35
36
  private whatsappConnected;
36
37
  private whatsappError;
37
38
  private webChatChannel;
39
+ private meshChannel;
38
40
  private autoRenewTimer;
39
41
  constructor(config: AppConfig);
40
42
  private getChatSetting;
@@ -66,6 +68,7 @@ export declare class Server {
66
68
  error?: string;
67
69
  };
68
70
  getWebChatChannel(): WebChatChannel | null;
71
+ getMeshChannel(): MeshChannel | null;
69
72
  triggerRestart(): Promise<void>;
70
73
  /**
71
74
  * Send a message to all known chats across all active channels.
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as y,loadWorkspaceFiles as S}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as 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 $,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as x}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 P}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as F}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{DebugA2UICommand as O}from"./commands/debuga2ui.js";import{DebugDynamicCommand as H}from"./commands/debugdynamic.js";import{CronService as B}from"./cron/cron-service.js";import{stripHeartbeatToken as L,isHeartbeatContentEffectivelyEmpty as Q}from"./cron/heartbeat-token.js";import{createServerToolsServer as W}from"./tools/server-tools.js";import{createCronToolsServer as z}from"./tools/cron-tools.js";import{createTTSToolsServer as G}from"./tools/tts-tools.js";import{createMemoryToolsServer as q}from"./tools/memory-tools.js";import{createBrowserToolsServer as V}from"./tools/browser-tools.js";import{createPicoToolsServer as J}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as X}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Y}from"./tools/concept-tools.js";import{BrowserService as Z}from"./browser/browser-service.js";import{MemorySearch as ee}from"./memory/memory-search.js";import{ConceptStore as te}from"./memory/concept-store.js";import{stripMediaLines as se}from"./utils/media-response.js";import{loadConfig as ne,loadRawConfig as oe,backupConfig as ie,resolveModelEntry as re,modelRefName as ae}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as he}from"./utils/logger.js";import{SessionErrorHandler as ge}from"./agent/session-error-handler.js";import{initStickerCache as le}from"./gateway/channels/telegram/stickers.js";const me=he("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir,e.timezone));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),le(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new Z,this.conceptStore=new te(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new B({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new ee(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),q(this.memorySearch);me.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return me.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return me.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};me.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;me.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const i={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},r=`${n}:${o}`;try{const s=await this.handleMessage(i);let n=s;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:o,text:i}=L(s,t);if(o)return me.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=i}if(t){const t=this.collectBroadcastTargets();me.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,n))),await Promise.allSettled(t.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(e.channel,e.chatId,n),await this.channelManager.releaseTyping(e.channel,e.chatId).catch(()=>{});return{response:n,delivered:!0}}finally{this.agentService.destroySession(r),this.sessionManager.resetSession(r),this.memoryManager&&this.memoryManager.clearSession(r),me.info(`Cron job "${e.name}": ephemeral session destroyed (${r})`)}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const i=o(this.config.dataDir,"HEARTBEAT.md");let r=!1;if(n(i))try{const t=e(i,"utf-8");r=!Q(t)}catch{r=!0}const a=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?a?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:r||a?{shouldRun:!0,reason:a?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new $(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:ae(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ae(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ae(s):void 0),fallbackActive:this.agentService.isFallbackActive(e),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 D(()=>this.agentService.getToolServers())),this.commandRegistry.register(new x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new H(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){me.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";me.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&me.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/"))return this.agentService.isBusy(s)?"I'm busy right now. Please resend this request later.":this.handleMessage({...e,text:e.text,__passthrough:!0});const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=u(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);me.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,c={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},h=S(this.config.dataDir),g={config:this.config,sessionContext:c,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},l=b(g),m=b({...g,mode:"minimal"});me.debug(`[${s}] System prompt (${l.length} chars): ${this.config.verboseDebugLogs?l:l.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,l,m,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return me.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=ge.analyzeError(r.error,r),c=ge.getRecoveryStrategy(a);return me.warn(`[${s}] ${c.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),c.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==c.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(me.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(me.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2}\]\n?/,"").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",se(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(me.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(me.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){me.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),me.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{me.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),me.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),me.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),me.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?me.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?me.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):me.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){me.info("Trigger restart requested");const e=ne();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();me.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),me.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){me.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(me.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>me.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){me.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){me.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}me.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){me.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir,e.timezone):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),me.info("Server reconfigured successfully")}async stop(){me.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),me.info("Server stopped")}}
1
+ 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 a}from"./auth/token-db.js";import{NodeSignatureDB as r}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}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{MeshChannel as d}from"./gateway/channels/mesh.js";import{ResponsesChannel as f}from"./channels/responses.js";import{AgentService as p}from"./agent/agent-service.js";import{SessionManager as u}from"./agent/session-manager.js";import{buildPrompt as b,buildSystemPrompt as y}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as w}from"./agent/workspace-files.js";import{NodeRegistry as v}from"./gateway/node-registry.js";import{MemoryManager as M}from"./memory/memory-manager.js";import{MessageProcessor as R}from"./media/message-processor.js";import{loadSTTProvider as C}from"./stt/stt-loader.js";import{CommandRegistry as A}from"./commands/command-registry.js";import{NewCommand as T}from"./commands/new.js";import{CompactCommand as k}from"./commands/compact.js";import{ModelCommand as $,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as D}from"./commands/stop.js";import{HelpCommand as I}from"./commands/help.js";import{McpCommand as x}from"./commands/mcp.js";import{ModelsCommand as E}from"./commands/models.js";import{CoderCommand as _}from"./commands/coder.js";import{SandboxCommand as P}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as U}from"./commands/customsubagents.js";import{StatusCommand as F}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as O}from"./commands/usage.js";import{DebugA2UICommand as H}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as L}from"./cron/cron-service.js";import{stripHeartbeatToken as Q,isHeartbeatContentEffectivelyEmpty as W}from"./cron/heartbeat-token.js";import{createServerToolsServer as z}from"./tools/server-tools.js";import{createCronToolsServer as G}from"./tools/cron-tools.js";import{createTTSToolsServer as q}from"./tools/tts-tools.js";import{createMemoryToolsServer as V}from"./tools/memory-tools.js";import{createBrowserToolsServer as J}from"./tools/browser-tools.js";import{createPicoToolsServer as X}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as Y}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Z}from"./tools/concept-tools.js";import{BrowserService as ee}from"./browser/browser-service.js";import{MemorySearch as te}from"./memory/memory-search.js";import{ConceptStore as se}from"./memory/concept-store.js";import{stripMediaLines as ne}from"./utils/media-response.js";import{loadConfig as oe,loadRawConfig as ie,backupConfig as ae,resolveModelEntry as re,modelRefName as he}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as ge}from"./utils/logger.js";import{SessionErrorHandler as le}from"./agent/session-error-handler.js";import{initStickerCache as me}from"./gateway/channels/telegram/stickers.js";const de=ge("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;meshChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new a(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new r(e.dbPath),this.nodeRegistry=new v,this.sessionManager=new u(this.sessionDb),e.memory.enabled&&(this.memoryManager=new M(e.memoryDir,e.timezone));const t=C(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,n),me(e.dataDir),this.commandRegistry=new A,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>z(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new ee,this.conceptStore=new se(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new p(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>q(()=>this.config):void 0,this.memorySearch?()=>V(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>J({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>X({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=w(this.config.dataDir);return y({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Y({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Z(this.conceptStore):void 0),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 L({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new te(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),V(this.memorySearch);de.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return de.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return de.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};de.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;de.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const i={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},a=`${n}:${o}`;try{const s=await this.handleMessage(i);let n=s;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:o,text:i}=Q(s,t);if(o)return de.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=i}if(t){const t=this.collectBroadcastTargets();de.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,n))),await Promise.allSettled(t.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(e.channel,e.chatId,n),await this.channelManager.releaseTyping(e.channel,e.chatId).catch(()=>{});return{response:n,delivered:!0}}finally{this.agentService.destroySession(a),this.sessionManager.resetSession(a),this.memoryManager&&this.memoryManager.clearSession(a),de.info(`Cron job "${e.name}": ephemeral session destroyed (${a})`)}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const i=o(this.config.dataDir,"HEARTBEAT.md");let a=!1;if(n(i))try{const t=e(i,"utf-8");a=!W(t)}catch{a=!0}const r=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?r?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:a||r?{shouldRun:!0,reason:r?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new T),this.commandRegistry.register(new k);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new $(()=>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,a=re(this.config,i),r=o(a?.name??he(i)),h=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const c=r||h;return c&&this.sessionManager.resetSession(e),c},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=ie(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]),ae(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new E(()=>this.config.models??[],e)),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new P(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 U(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new F(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:he(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??he(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?he(s):void 0),fallbackActive:this.agentService.isFallbackActive(e),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 D(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getToolServers())),this.commandRegistry.register(new I(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new O(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new H(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){de.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 f({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}if(this.config.channels.mesh.enabled){const e=this.config.channels.mesh;e.agentId&&e.privateKey?(this.meshChannel||(this.meshChannel=new d({brokerUrl:e.brokerUrl,agentId:e.agentId,privateKey:e.privateKey,reconnectDelayMs:e.reconnectDelayMs})),this.channelManager.registerAdapter(this.meshChannel)):de.warn("Mesh channel enabled but agentId or privateKey not configured")}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";de.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&de.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/"))return this.agentService.isBusy(s)?"I'm busy right now. Please resend this request later.":this.handleMessage({...e,text:e.text,__passthrough:!0});const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),a=b(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);de.debug(`[${s}] Prompt to agent (${a.text.length} chars): ${this.config.verboseDebugLogs?a.text:a.text.slice(0,15)+"..."}${a.images.length>0?` [+${a.images.length} image(s)]`:""}`);const r=o.model,h={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},c=w(this.config.dataDir),g={config:this.config,sessionContext:h,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},l=y(g),m=y({...g,mode:"minimal"});de.debug(`[${s}] System prompt (${l.length} chars): ${this.config.verboseDebugLogs?l:l.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,a,o.sessionId,l,m,r,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return de.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",a={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},r=le.analyzeError(a.error,a),h=le.getRecoveryStrategy(r);return de.warn(`[${s}] ${h.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),h.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==h.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(de.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(de.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(a.text||"[media]").replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2}\]\n?/,"").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",ne(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(de.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(de.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){de.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"),this.config.channels.mesh.enabled&&e.push("mesh"),de.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{de.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),de.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}),de.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}),de.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?de.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?de.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):de.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}getMeshChannel(){return this.meshChannel}async triggerRestart(){de.info("Trigger restart requested");const e=oe();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();de.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),de.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){de.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&&(de.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>de.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){de.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){de.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}de.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){de.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 M(e.memoryDir,e.timezone):this.memoryManager=null;const t=C(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,s),this.commandRegistry=new A,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>z(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new p(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>q(()=>this.config):void 0,this.memorySearch?()=>V(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>J({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>X({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=w(this.config.dataDir);return y({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Y({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Z(this.conceptStore):void 0),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),de.info("Server reconfigured successfully")}async stop(){de.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),de.info("Server stopped")}}
@@ -50,6 +50,8 @@ If SOUL.md is present, embody its persona and tone.
50
50
 
51
51
  {{WORKSPACE_FILES}}
52
52
 
53
+ {{MESH_INFO}}
54
+
53
55
  # Runtime
54
56
 
55
57
  {{RUNTIME_LINE}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.36",
3
+ "version": "1.6.39",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",