@hera-al/server 1.6.39 → 1.6.41
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{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createLogger as l}from"../utils/logger.js";const c=l("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=
|
|
1
|
+
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createLogger as l}from"../utils/logger.js";const c=l("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=s=>e(a,()=>this.config,h,s)),l&&(this.serverToolsFactory=l),c&&(this.cronToolsFactory=c),g&&(this.ttsToolsFactory=g),y&&(this.memoryToolsFactory=y),u&&(this.browserToolsFactory=u),T&&(this.picoToolsFactory=T),m&&(this.plasmaClientToolsFactory=m),F&&(this.conceptToolsFactory=F),d&&(this.showToolUseGetter=d)}async sendMessage(s,o,t,e,r,i,n,a,l,h){const g=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);if(i&&i!==g.getModel()){const t=s=>{const o=this.config.models.find(o=>o.id===s);return!(!o?.proxy||"not-used"===o.proxy)},y=t(g.getModel()),d=t(i);if(y||d){c.info(`[${s}] Proxy config change detected (old=${y}, new=${d}), restarting session`),this.destroySession(s);const t=this.getOrCreateAgent(s,void 0,e,r,i,n,a,l,h);return await t.send(o)}await g.setModel(i)}try{return await g.send(o)}catch(y){const d=y instanceof Error?y.message:String(y);if(d.includes("INVALID_ARGUMENT")||/API Error: 400/.test(d)){c.warn(`[${s}] Transient API 400 error, retrying once: ${d.slice(0,120)}`),this.agents.delete(s),g.close();const y=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);try{return await y.send(o)}catch(o){c.error(`[${s}] Retry also failed: ${o}`),this.agents.delete(s),y.close();const e=o instanceof Error?o.message:String(o);if(t)return{response:e.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw o}}if(this.agents.delete(s),g.close(),t)return c.warn(`Session agent failed for ${s}: ${y}`),{response:d.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw y}}hasNodeTools(){return null!==this.nodeToolsFactory}hasMessageTools(){return null!==this.messageToolsFactory}getToolServers(){if(!this.refToolServers){const s=[];this.nodeToolsFactory&&s.push(this.nodeToolsFactory()),this.messageToolsFactory&&s.push(this.messageToolsFactory()),this.serverToolsFactory&&s.push(this.serverToolsFactory()),this.cronToolsFactory&&s.push(this.cronToolsFactory()),this.ttsToolsFactory&&s.push(this.ttsToolsFactory()),this.memoryToolsFactory&&s.push(this.memoryToolsFactory()),this.browserToolsFactory&&s.push(this.browserToolsFactory()),this.picoToolsFactory&&s.push(this.picoToolsFactory()),this.telegramToolsFactory&&s.push(this.telegramToolsFactory()),this.a2uiToolsFactory&&s.push(this.a2uiToolsFactory()),this.dynamicUIToolsFactory&&s.push(this.dynamicUIToolsFactory()),this.plasmaClientToolsFactory&&s.push(this.plasmaClientToolsFactory()),this.conceptToolsFactory&&s.push(this.conceptToolsFactory()),s.push(a("__ref__",this.config.dataDir)),this.refToolServers=s}return this.refToolServers}getOrCreateAgent(s,t,e,r,i,l,c,h,g){const y=this.agents.get(s);if(y&&y.isActive())return y;let d,u;y&&(y.close(),this.agents.delete(s));const T=s.indexOf(":"),m=T>0?s.substring(0,T):void 0,F=T>0?s.substring(T+1):void 0;this.nodeRegistry&&(m&&F?d=n({nodeRegistry:this.nodeRegistry,channel:m,chatId:F}):this.dynamicUIToolsFactory&&(d=this.dynamicUIToolsFactory())),this.plasmaClientToolsFactory&&(u=this.plasmaClientToolsFactory(m,F));const p=a(s,this.config.dataDir),f=new o(s,this.config,e,r,t,i,this.nodeToolsFactory?.()??void 0,this.messageToolsFactory?.(s)??void 0,this.serverToolsFactory?.()??void 0,this.cronToolsFactory?.()??void 0,l,c,h,this.ttsToolsFactory?.()??void 0,this.memoryToolsFactory?.()??void 0,this.browserToolsFactory?.()??void 0,g,this.picoToolsFactory?.()??void 0,this.telegramToolsFactory?.()??void 0,this.a2uiToolsFactory?.()??void 0,d??void 0,u??void 0,this.conceptToolsFactory?.()??void 0,p);if(this.channelManager){const s=this.channelManager;if(f.setChannelSender(async(o,t,e,r)=>{r&&r.length>0?await s.sendButtons(o,t,e,[r]):await s.sendToChannel(o,t,e)}),this.showToolUseGetter){const o=this.showToolUseGetter;f.setToolUseNotifier(async(t,e,r)=>{if(!o(`${t}:${e}`))return;const i=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await s.sendToChannel(t,e,i),await s.setTyping(t,e)})}f.setTypingSetter(async(o,t)=>{await s.setTyping(o,t)}),f.setTypingClearer(async(o,t)=>{await s.clearTyping(o,t)}),f.setTextBlockStreamer(async(o,t,e)=>{await s.sendResponse(o,t,e)})}return f.setUsageRecorder((s,o,t,e,r)=>{this.usageBySession.set(s,{totalCostUsd:o,durationMs:t,numTurns:e,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(s,f),f}async interrupt(s){const o=this.agents.get(s);return!!o&&o.interrupt()}isBusy(s){const o=this.agents.get(s);return!!o&&o.isBusy()}hasPendingPermission(s){const o=this.agents.get(s);return!!o&&o.hasPendingPermission()}isFallbackActive(s){const o=this.agents.get(s);return!!o&&o.isFallbackActive()}resolvePermission(s,o){const t=this.agents.get(s);t&&t.resolvePermission(o)}hasPendingQuestion(s){const o=this.agents.get(s);return!!o&&o.hasPendingQuestion()}resolveQuestion(s,o){const t=this.agents.get(s);t&&t.resolveQuestion(o)}destroySession(s){const o=this.agents.get(s);o&&(o.close(),this.agents.delete(s),c.info(`Session agent destroyed: ${s}`))}destroyAll(){for(const[s,o]of this.agents)o.close(),c.info(`Session agent destroyed (reconfigure): ${s}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(s=>{const o=this.agents.get(s);return o&&o.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(s){return this.usageBySession.get(s)}getSdkSlashCommands(){for(const s of this.agents.values()){const o=s.getSdkSlashCommands();if(o.length>0)return o}return[]}async listModels(){try{const o=s({prompt:"list models",options:{maxTurns:0}}),t=await o.supportedModels();for await(const s of o)break;return t.map(s=>({id:s.id??s.name??String(s),name:s.name??s.id??String(s)}))}catch(s){return c.error(`Failed to list models: ${s}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
|
|
@@ -23,9 +23,16 @@ export declare class MeshChannel implements ChannelAdapter {
|
|
|
23
23
|
private reconnectDelay;
|
|
24
24
|
private stopping;
|
|
25
25
|
private pingTimer;
|
|
26
|
-
/** Track message IDs we sent, so we can
|
|
26
|
+
/** Track message IDs we sent → origin session key, so we can route replies back. */
|
|
27
27
|
private pendingOutbound;
|
|
28
|
+
/** Callback invoked when a reply arrives for an outbound message we sent. */
|
|
29
|
+
private replyCallback;
|
|
28
30
|
constructor(config: MeshChannelConfig);
|
|
31
|
+
/**
|
|
32
|
+
* Register a callback for when a reply arrives for an outbound message.
|
|
33
|
+
* The callback receives the origin session key, the agent ID that replied, and the reply text.
|
|
34
|
+
*/
|
|
35
|
+
onReply(callback: (originSessionKey: string, fromAgent: string, text: string) => void): void;
|
|
29
36
|
start(onMessage: MessageHandler): Promise<void>;
|
|
30
37
|
private connect;
|
|
31
38
|
/**
|
|
@@ -36,7 +43,7 @@ export declare class MeshChannel implements ChannelAdapter {
|
|
|
36
43
|
* If it's a new message (no replyTo matching our outbound), dispatch AND auto-reply.
|
|
37
44
|
*/
|
|
38
45
|
private handleMeshMessage;
|
|
39
|
-
sendText(chatId: string, text: string): Promise<void>;
|
|
46
|
+
sendText(chatId: string, text: string, originSessionKey?: string): Promise<void>;
|
|
40
47
|
/**
|
|
41
48
|
* Send a typed mesh message (for MCP tools).
|
|
42
49
|
*/
|
|
@@ -1 +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
|
|
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 Map;replyCallback=null;constructor(e){this.config=e,this.reconnectDelay=e.reconnectDelayMs??5e3}onReply(e){this.replyCallback=e}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),i=o&&t.replyTo?this.pendingOutbound.get(t.replyTo):void 0;let r;if(t.replyTo&&this.pendingOutbound.has(t.replyTo)&&this.pendingOutbound.delete(t.replyTo),"string"==typeof t.payload)r=t.payload;else if(t.payload&&"object"==typeof t.payload){const e=t.payload;r="string"==typeof e.text?e.text:`[Mesh ${t.type}] ${JSON.stringify(t.payload)}`}else r=`[Mesh ${t.type}]`;if(o){if(s.info(`Reply from ${t.from} (to outbound ${t.replyTo}) → routing to origin session${i?` [${i}]`:""}`),i&&this.replyCallback)return void this.replyCallback(i,t.from,r)}else s.info(`New message from ${t.from}: ${t.type} → dispatching to agent (will auto-reply)`);const a={chatId:n,userId:t.from,channelName:"mesh",text:r,attachments:[],username:t.from};try{const i=await this.onMessage(a);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,o){if(!this.connected||!this.ws)return void s.warn(`Cannot send to ${t}: mesh not connected`);const i=e.randomUUID(),r={id:i,from:this.config.agentId,to:t,type:"text",payload:{text:n},timestamp:Date.now()};this.pendingOutbound.set(i,o),this.sendRaw({type:"message",message:r}),s.debug(`Sent to ${t} (id=${i}${o?`, origin=${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")}}
|
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 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")}}
|
|
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 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 $}from"./commands/compact.js";import{ModelCommand as k,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as I}from"./commands/stop.js";import{HelpCommand as x}from"./commands/help.js";import{McpCommand as D}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 re,resolveModelEntry as ae,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 r(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new a(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=ae(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:[]},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}=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(r),this.sessionManager.resetSession(r),this.memoryManager&&this.memoryManager.clearSession(r),de.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=!W(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 T),this.commandRegistry.register(new $);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=ae(this.config,i),a=o(r?.name??he(i)),h=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const c=a||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]),re(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=ae(this.config,t),o=s?ae(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 I(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 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.meshChannel.onReply((e,t,s)=>{de.info(`[Mesh] Routing reply from ${t} to origin session ${e}`);const n=e.indexOf(":"),o=n>0?e.substring(0,n):"mesh",i=n>0?e.substring(n+1):t,r={chatId:i,userId:t,channelName:o,text:`[Mesh reply from ${t}] ${s}`,attachments:[],username:t};this.handleMessage(r).then(t=>{t&&t.trim()&&this.channelManager.sendResponse(o,i,t).catch(t=>{de.error(`[Mesh] Failed to deliver reply-response to ${e}: ${t}`)})}).catch(s=>{de.error(`[Mesh] Failed to process reply from ${t} in session ${e}: ${s}`)})})),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),r=b(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);de.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,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,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 de.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=le.analyzeError(r.error,r),h=le.getRecoveryStrategy(a);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=(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",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")}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppConfig } from "../config.js";
|
|
2
2
|
import type { ChannelManager } from "../gateway/channel-manager.js";
|
|
3
3
|
import type { SessionDB } from "../agent/session-db.js";
|
|
4
|
-
export declare function createMessageToolsServer(channelManager: ChannelManager, getConfig: () => AppConfig, sessionDb: SessionDB): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
4
|
+
export declare function createMessageToolsServer(channelManager: ChannelManager, getConfig: () => AppConfig, sessionDb: SessionDB, originSessionKey?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
5
5
|
//# sourceMappingURL=message-tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r,i){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{if(e.buttons&&e.buttons.length>0)await s.sendButtons(e.channel,e.chatId,e.text,[e.buttons]);else if("mesh"===e.channel&&i){const t=s.getAdapter("mesh");t?await t.sendText(e.chatId,e.text,i):await s.sendResponse(e.channel,e.chatId,e.text)}else await s.sendResponse(e.channel,e.chatId,e.text);return a.info(`Message sent to ${e.channel}:${e.chatId} (${e.text.length} chars)`),{content:[{type:"text",text:`Message sent to ${e.channel}:${e.chatId}`}]}}catch(t){const n=t instanceof Error?t.message:String(t);return a.error(`Failed to send message to ${e.channel}:${e.chatId}: ${n}`),{content:[{type:"text",text:`Error sending message: ${n}`}],isError:!0}}}),t("list_models","List all models in the registry with their name, model ID, API base URL, and API key environment variable name.",{},async()=>{const e=(o().models||[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy});if(0===e.length)return{content:[{type:"text",text:"No models in registry."}]};const t=e.map(e=>({name:e.name,modelId:e.id,type:(e.types||["external"])[0],baseURL:e.baseURL||"",apiKeyEnvVar:e.useEnvVar||((e.types||["external"]).includes("internal")?"":"OPENAI_API_KEY")}));return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}),t("list_channels","List all registered channels, whether they are currently active, and known chat IDs for each channel (from config allowFrom + session history).",{},async()=>{const e=s.listAdapters();if(0===e.length)return{content:[{type:"text",text:"No channels registered."}]};const t=o(),n=r.listSessions(),a=e.map(e=>{const s=new Set,a=[],o=t.channels[e.name];if(o?.accounts)for(const e of Object.values(o.accounts)){const t=e?.allowFrom;if(Array.isArray(t))for(const e of t){const t=String(e).trim();t&&!s.has(t)&&(s.add(t),a.push({id:t,source:"config"}))}}for(const t of n){const n=t.sessionKey.indexOf(":");if(n<0)continue;const o=t.sessionKey.substring(0,n),r=t.sessionKey.substring(n+1);o===e.name&&r&&!s.has(r)&&(s.add(r),a.push({id:r,source:"session"}))}return{name:e.name,active:e.active,chatIds:a}});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}})]})}
|