@hera-al/server 1.6.18 → 1.6.23

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,filterForSubagent as l}from"./workspace-files.js";import{createLogger as c}from"../utils/logger.js";const d=c("PromptBuilder");export function buildSystemPrompt(c){const{config:u,sessionContext:p,mode:g,hasNodeTools:f,hasMessageTools:y,coderSkill:T,subagentTask:_,toolServers:E}=c,I="minimal"===g?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",S=a(u.dataDir,I),w="minimal"===g?l(c.workspaceFiles):c.workspaceFiles;var b;const O=resolvePlaceholders(S,{SESSION_KEY:p.sessionKey,CHANNEL:p.channel,CHAT_ID:p.chatId,SESSION_ID:p.sessionId||"(new session)",MODEL:s(u.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:u.agent.workspacePath,DATA_DIR:u.dataDir,MEMORY_FILE:p.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:p.attachmentsDir||"(memory disabled)",TIMEZONE:u.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,SUBAGENT_TASK:_??"",NODE_TOOLS_INSTRUCTIONS:f?"# 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.":"",MESSAGE_TOOLS_INSTRUCTIONS:y?"# 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.":"",HEARTBEAT_INSTRUCTIONS:u.cron.enabled?(b=u.cron.heartbeat.message,`# Heartbeats\n\nHeartbeat prompt: ${b}\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.`):"",HEARTBEAT_PROMPT:u.cron.enabled?u.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:T?"":r(u.dataDir,g),SEARCH_IN_MEMORIES:"builtin-only"!==u.memory.recallStrategy?"## 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.":"",AVAILABLE_TOOLS:m(E),RUNTIME_LINE:h(u,p),WORKSPACE_FILES:i(w)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${g}, template=${I}, length=${O.length})`),O}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function m(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 h(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){const o=[],s=[];n&&o.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?o.push(n.text):"image"===n.type&&n.imageBase64&&s.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});if(e.savedFiles.length>0){o.push(""),o.push("Files available in the current working directory:");for(const n of e.savedFiles)o.push(`- ${n}`)}return{text:o.join("\n"),images:s}}
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,filterForSubagent as l}from"./workspace-files.js";import{createLogger as c}from"../utils/logger.js";const d=c("PromptBuilder");export function buildSystemPrompt(c){const{config:u,sessionContext:p,mode:g,hasNodeTools:f,hasMessageTools:y,coderSkill:T,subagentTask:_,toolServers:E}=c,I="minimal"===g?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",S=a(u.dataDir,I),w="minimal"===g?l(c.workspaceFiles):c.workspaceFiles;var b;const O=resolvePlaceholders(S,{SESSION_KEY:p.sessionKey,CHANNEL:p.channel,CHAT_ID:p.chatId,SESSION_ID:p.sessionId||"(new session)",MODEL:s(u.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:u.agent.workspacePath,DATA_DIR:u.dataDir,MEMORY_FILE:p.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:p.attachmentsDir||"(memory disabled)",TIMEZONE:u.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,SUBAGENT_TASK:_??"",NODE_TOOLS_INSTRUCTIONS:f?"# 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.":"",MESSAGE_TOOLS_INSTRUCTIONS:y?"# 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.":"",HEARTBEAT_INSTRUCTIONS:u.cron.enabled?(b=u.cron.heartbeat.message,`# Heartbeats\n\nHeartbeat prompt: ${b}\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.`):"",HEARTBEAT_PROMPT:u.cron.enabled?u.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:T?"":r(u.dataDir,g),SEARCH_IN_MEMORIES:"builtin-only"!==u.memory.recallStrategy?"## 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.":"",AVAILABLE_TOOLS:m(E),RUNTIME_LINE:h(u,p),WORKSPACE_FILES:i(w)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${g}, template=${I}, length=${O.length})`),O}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function m(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 h(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){const o=[],s=[];n&&o.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?o.push(n.text):"image"===n.type&&n.imageBase64&&s.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});const a=e.savedFiles.filter(e=>!e.endsWith(".tgs"));if(a.length>0){o.push(""),o.push("Files available in the current working directory:");for(const e of a)o.push(`- ${e}`)}return{text:o.join("\n"),images:s}}
@@ -1 +1 @@
1
- import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w,T,S,R,x){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const K=a??i.agent.model;this.model=K?t(i,K):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const _=i.agent.mainFallback;_&&(this.opts.fallbackModel=t(i,_)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const k={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(k,i.agent.mcpServers),h&&(k["node-tools"]=h),u&&(k["message-tools"]=u),c&&(k["server-tools"]=c),d&&(k["cron-tools"]=d),f&&(k["tts-tools"]=f),y&&(k["memory-tools"]=y),$&&(k["browser-tools"]=$),v&&(k["pico-tools"]=v),w&&(k["telegram-actions"]=w),T&&(k["a2ui-tools"]=T),S&&(k["dynamic-ui-tools"]=S),R&&(k["plasma-client-tools"]=R),x&&(k["concept-tools"]=x),Object.keys(k).length>0&&(this.opts.mcpServers=k,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(k)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const A=i.agent.plugins.filter(e=>e.enabled);A.length>0&&(this.opts.options={...this.opts.options,plugins:A.map(e=>({type:"local",path:e.path}))});const C=this.buildEnvForModel(this.model);this.opts.env=C.env,C.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.fullResponse??i.response,r=SessionAgent.API_RETRY_RE.test(n),l=this.config.agent.apiRetry,a=l.maxAttempts;if(r&&t<a){const s=t+1,i=Math.min(l.baseDelayMs*2**t,l.maxDelayMs);if(o.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${a} in ${i}ms`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/${a})...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,i)),this.send(e,s)}if(r&&t>=a&&(o.error(`[${this.sessionKey}] API error persists after ${a} retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${a} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...i}=s;this.currentResponse=JSON.stringify(i,null,2),o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.slice(0,200)}`)}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);o.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
1
+ import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w,T,S,R,K){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const x=a??i.agent.model;this.model=x?t(i,x):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const _=i.agent.mainFallback;_&&(this.opts.fallbackModel=t(i,_)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const k={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(k,i.agent.mcpServers),h&&(k["node-tools"]=h),u&&(k["message-tools"]=u),c&&(k["server-tools"]=c),d&&(k["cron-tools"]=d),f&&(k["tts-tools"]=f),y&&(k["memory-tools"]=y),$&&(k["browser-tools"]=$),v&&(k["pico-tools"]=v),w&&(k["telegram-actions"]=w),T&&(k["a2ui-tools"]=T),S&&(k["dynamic-ui-tools"]=S),R&&(k["plasma-client-tools"]=R),K&&(k["concept-tools"]=K),Object.keys(k).length>0&&(this.opts.mcpServers=k,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(k)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const A=i.agent.plugins.filter(e=>e.enabled);A.length>0&&(this.opts.options={...this.opts.options,plugins:A.map(e=>({type:"local",path:e.path}))});const C=this.buildEnvForModel(this.model);this.opts.env=C.env,C.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}static API_RETRY_RE=/API Error:\s*(?:500|502|503|529)|overloaded|internal server error/i;async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.fullResponse??i.response,r=SessionAgent.API_RETRY_RE.test(n),l=this.config.agent.apiRetry,a=l.maxAttempts;if(r&&t<a){const s=t+1,i=Math.min(l.baseDelayMs*2**t,l.maxDelayMs);if(o.warn(`[${this.sessionKey}] Transient API error detected, retry ${s}/${a} in ${i}ms`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/${a})...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,i)),this.send(e,s)}if(r&&t>=a&&(o.error(`[${this.sessionKey}] API error persists after ${a} retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e=`API did not respond after ${a} attempts. Please try again later.`;this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const e=new Set(["task_started","task_notification","files_persisted","hook_started","hook_progress","hook_response"]),{type:i,...n}=s,r=JSON.stringify(n,null,2);e.has(t)?o.debug(`[${this.sessionKey}] Internal SDK event (${t}): ${r.slice(0,200)}`):(this.currentResponse=r,o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${r.slice(0,200)}`))}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);o.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
@@ -1 +1 @@
1
- import{createLogger as e}from"../utils/logger.js";const r=e("SessionErrorHandler");export var SessionErrorType;!function(e){e.AGENT_CLOSED="AGENT_CLOSED",e.SESSION_CORRUPTED="SESSION_CORRUPTED",e.SESSION_NOT_FOUND="SESSION_NOT_FOUND",e.API_ERROR="API_ERROR",e.NETWORK_ERROR="NETWORK_ERROR",e.UNKNOWN="UNKNOWN"}(SessionErrorType||(SessionErrorType={}));export class SessionErrorHandler{static analyzeError(e,s){const o=e.message.toLowerCase();return o.includes("sessionagent closed")||o.includes("agent closed")||o.includes("closed")?(r.debug(`[${s.sessionKey}] Error type: AGENT_CLOSED`),SessionErrorType.AGENT_CLOSED):o.includes("session not found")||o.includes("invalid session")||o.includes("session does not exist")||o.includes("no conversation found")?(r.debug(`[${s.sessionKey}] Error type: SESSION_NOT_FOUND`),SessionErrorType.SESSION_NOT_FOUND):o.includes("api error")||o.includes("unauthorized")||o.includes("rate limit")?(r.debug(`[${s.sessionKey}] Error type: API_ERROR`),SessionErrorType.API_ERROR):o.includes("econnrefused")||o.includes("timeout")||o.includes("network")?(r.debug(`[${s.sessionKey}] Error type: NETWORK_ERROR`),SessionErrorType.NETWORK_ERROR):o.includes("corrupted")||o.includes("invalid state")||o.includes("resume failed")?(r.debug(`[${s.sessionKey}] Error type: SESSION_CORRUPTED`),SessionErrorType.SESSION_CORRUPTED):(r.debug(`[${s.sessionKey}] Error type: UNKNOWN - ${o}`),SessionErrorType.UNKNOWN)}static getRecoveryStrategy(e){switch(e){case SessionErrorType.AGENT_CLOSED:return{action:"retry_with_new_agent",clearSession:!1,notifyUser:!1,logLevel:"info",message:"Agent was closed (likely due to restart), retrying with new agent"};case SessionErrorType.SESSION_NOT_FOUND:case SessionErrorType.SESSION_CORRUPTED:return{action:"clear_and_retry",clearSession:!0,notifyUser:!1,logLevel:"warn",message:"Session invalid, starting fresh"};case SessionErrorType.API_ERROR:return{action:"retry_with_backoff",clearSession:!1,notifyUser:!0,logLevel:"error",message:"API error, retrying with backoff"};case SessionErrorType.NETWORK_ERROR:return{action:"retry_immediate",clearSession:!1,notifyUser:!1,logLevel:"warn",message:"Network error, retrying immediately"};default:return{action:"fallback",clearSession:!0,notifyUser:!0,logLevel:"error",message:"Unknown error, falling back to session reset"}}}}
1
+ import{createLogger as e}from"../utils/logger.js";const r=e("SessionErrorHandler");export var SessionErrorType;!function(e){e.AGENT_CLOSED="AGENT_CLOSED",e.SESSION_CORRUPTED="SESSION_CORRUPTED",e.SESSION_NOT_FOUND="SESSION_NOT_FOUND",e.API_ERROR="API_ERROR",e.NETWORK_ERROR="NETWORK_ERROR",e.UNKNOWN="UNKNOWN"}(SessionErrorType||(SessionErrorType={}));export class SessionErrorHandler{static analyzeError(e,s){const o=e.message.toLowerCase();return o.includes("sessionagent closed")||o.includes("agent closed")||o.includes("closed")?(r.debug(`[${s.sessionKey}] Error type: AGENT_CLOSED`),SessionErrorType.AGENT_CLOSED):o.includes("session not found")||o.includes("invalid session")||o.includes("session does not exist")||o.includes("no conversation found")?(r.debug(`[${s.sessionKey}] Error type: SESSION_NOT_FOUND`),SessionErrorType.SESSION_NOT_FOUND):o.includes("api error")||o.includes("unauthorized")||o.includes("rate limit")?(r.debug(`[${s.sessionKey}] Error type: API_ERROR`),SessionErrorType.API_ERROR):o.includes("econnrefused")||o.includes("timeout")||o.includes("network")?(r.debug(`[${s.sessionKey}] Error type: NETWORK_ERROR`),SessionErrorType.NETWORK_ERROR):o.includes("corrupted")||o.includes("invalid state")||o.includes("resume failed")||o.includes("could not process image")?(r.debug(`[${s.sessionKey}] Error type: SESSION_CORRUPTED`),SessionErrorType.SESSION_CORRUPTED):(r.debug(`[${s.sessionKey}] Error type: UNKNOWN - ${o}`),SessionErrorType.UNKNOWN)}static getRecoveryStrategy(e){switch(e){case SessionErrorType.AGENT_CLOSED:return{action:"retry_with_new_agent",clearSession:!1,notifyUser:!1,logLevel:"info",message:"Agent was closed (likely due to restart), retrying with new agent"};case SessionErrorType.SESSION_NOT_FOUND:case SessionErrorType.SESSION_CORRUPTED:return{action:"clear_and_retry",clearSession:!0,notifyUser:!1,logLevel:"warn",message:"Session invalid, starting fresh"};case SessionErrorType.API_ERROR:return{action:"retry_with_backoff",clearSession:!1,notifyUser:!0,logLevel:"error",message:"API error, retrying with backoff"};case SessionErrorType.NETWORK_ERROR:return{action:"retry_immediate",clearSession:!1,notifyUser:!1,logLevel:"warn",message:"Network error, retrying immediately"};default:return{action:"fallback",clearSession:!0,notifyUser:!0,logLevel:"error",message:"Unknown error, falling back to session reset"}}}}
@@ -1,5 +1,6 @@
1
1
  import type { Command, CommandContext, CommandResult } from "./command.js";
2
2
  export interface StatusInfo {
3
+ configDefaultModel: string;
3
4
  agentModel: string;
4
5
  agentModelName?: string;
5
6
  fallbackModel: string;
@@ -1 +1 @@
1
- export class StatusCommand{provider;name="status";description="Show current server status (models, coder, nodes)";constructor(e){this.provider=e}async execute(e){const o=this.provider(e.sessionKey),s=[];s.push("Server Status");const n=o.agentModelName?`${o.agentModelName} (${o.agentModel})`:o.agentModel;if(s.push(` Agent model: ${n}`),o.fallbackModel){const e=o.fallbackModelName?`${o.fallbackModelName} (${o.fallbackModel})`:o.fallbackModel;s.push(` Fallback model: ${e}`)}else s.push(" Fallback model: (none)");if(s.push(" Coder skill: "+(o.coderSkill?"on":"off")),s.push(" Show tool use: "+(o.showToolUse?"on":"off")),s.push(" SubAgents: "+(o.subagentsEnabled?"on":"off")),s.push(" Custom SubAgents: "+(o.customSubAgentsEnabled?"on":"off")),s.push(" Sandbox: "+(o.sandboxEnabled?"on":"off")),o.connectedNodes.length>0){s.push(` Connected nodes (${o.connectedNodes.length}):`);for(const e of o.connectedNodes){const o=e.displayName??e.nodeId,n=e.hostname?` (${e.hostname})`:"";s.push(` - ${o}${n}`)}}else s.push(" Connected nodes: none");return{text:s.join("\n")}}}
1
+ export class StatusCommand{provider;name="status";description="Show current server status (models, coder, nodes)";constructor(e){this.provider=e}async execute(e){const o=this.provider(e.sessionKey),s=[];s.push("Server Status"),s.push(` Config default: ${o.configDefaultModel}`);const n=o.agentModelName?`${o.agentModelName} (${o.agentModel})`:o.agentModel;if(s.push(` Agent model: ${n}`),o.fallbackModel){const e=o.fallbackModelName?`${o.fallbackModelName} (${o.fallbackModel})`:o.fallbackModel;s.push(` Fallback model: ${e}`)}else s.push(" Fallback model: (none)");if(s.push(" Coder skill: "+(o.coderSkill?"on":"off")),s.push(" Show tool use: "+(o.showToolUse?"on":"off")),s.push(" SubAgents: "+(o.subagentsEnabled?"on":"off")),s.push(" Custom SubAgents: "+(o.customSubAgentsEnabled?"on":"off")),s.push(" Sandbox: "+(o.sandboxEnabled?"on":"off")),o.connectedNodes.length>0){s.push(` Connected nodes (${o.connectedNodes.length}):`);for(const e of o.connectedNodes){const o=e.displayName??e.nodeId,n=e.hostname?` (${e.hostname})`:"";s.push(` - ${o}${n}`)}}else s.push(" Connected nodes: none");return{text:s.join("\n")}}}
@@ -1 +1 @@
1
- import{Bot as t,InputFile as e}from"grammy";import{validateChannelUser as i}from"../../../auth/auth-middleware.js";import{parseMediaLines as n}from"../../../utils/media-response.js";import{markdownToTelegramHtmlChunks as a}from"../../../utils/telegram-format.js";import{createLogger as o}from"../../../utils/logger.js";import{sendMessageTelegram as s}from"./send.js";import{reactMessageTelegram as r,resolveReactionLevel as c}from"./reactions.js";import{editMessageTelegram as l,deleteMessageTelegram as d}from"./edit-delete.js";import{sendStickerTelegram as h,cacheSticker as g}from"./stickers.js";import{validateButtonsForChatId as m}from"./inline-buttons.js";const f=o("Telegram");export class TelegramChannel{name="telegram";bot;config;accountId;tokenDb;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(e,i,n,a=!0){this.config=e,this.accountId=i,this.tokenDb=n,this.inflightTyping=a,this.bot=new t(e.botToken)}async start(t){this.bot.on("message",e=>{this.handleIncoming(e,t)}),this.bot.on("callback_query:data",e=>{const i=e.callbackQuery.data,n=String(e.from?.id??"unknown"),a=String(e.chat?.id??e.callbackQuery.message?.chat?.id??"unknown"),o=e.from?.username;e.answerCallbackQuery().catch(()=>{}),this.startTypingInterval(a);t({chatId:a,userId:n,channelName:"telegram",text:i,attachments:[],username:o,rawContext:e}).then(async t=>{t&&t.trim()&&await this.sendText(a,t)}).catch(t=>{f.error(`Error handling callback from ${n}: ${t}`)})}),f.info("Starting Telegram bot..."),this.bot.start({onStart:t=>{f.info(`Telegram bot started: @${t.username}`)}}).catch(t=>{String(t).includes("Aborted delay")?f.debug("Telegram polling stopped"):f.error(`Telegram bot polling error: ${t}`)})}async sendText(t,e){try{await s(t,e,{token:this.config.botToken,accountId:this.accountId,textChunkLimit:this.config.textChunkLimit,linkPreview:this.config.linkPreview,retry:this.config.retry})}catch(t){throw f.error(`Failed to send message: ${t}`),t}await this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?await this.bot.api.sendChatAction(t,"typing").catch(()=>{}):this.startTypingInterval(t)}async resendTypingIfActive(t){this.typingIntervals.has(t)&&await this.bot.api.sendChatAction(t,"typing").catch(()=>{})}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t))}async releaseTyping(t){this.stopTypingInterval(t)}startTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.bot.api.sendChatAction(t,"typing").catch(()=>{});const i=setInterval(()=>{const e=this.inflightCount.get(t)??0;!this.inflightTyping||e>0?this.bot.api.sendChatAction(t,"typing").catch(()=>{}):(clearInterval(i),this.typingIntervals.delete(t))},4e3);this.typingIntervals.set(t,i)}stopTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??1)-1;return void(e>0?this.inflightCount.set(t,e):this.inflightCount.delete(t))}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t))}async sendButtons(t,e,i){const n=i.map(t=>t.map(t=>({text:t.text,callback_data:t.callbackData??t.text})));try{m(n,this.config,t)}catch(i){return f.warn(`Button validation failed: ${i}, sending without buttons`),void await this.sendText(t,e)}await s(t,e,{token:this.config.botToken,accountId:this.accountId,buttons:n,textChunkLimit:this.config.textChunkLimit,linkPreview:this.config.linkPreview,retry:this.config.retry}),await this.resendTypingIfActive(t)}async sendAudio(t,i,n){const a=new e(i);n?await this.bot.api.sendVoice(t,a):await this.bot.api.sendAudio(t,a),await this.resendTypingIfActive(t)}async reactMessage(t,e,i,n){const{level:a}=c(this.config);"off"!==a?await r(t,e,i,this.config.botToken,{remove:n}):f.debug("Reactions disabled for this account")}async editMessage(t,e,i,n){const a=n?.map(t=>({text:t.text,callback_data:t.callbackData??t.text}));await l(t,e,i,this.config.botToken,{buttons:a?[a]:void 0,linkPreview:this.config.linkPreview})}async deleteMessage(t,e){await d(t,e,this.config.botToken,this.config.retry)}async sendSticker(t,e){await h(t,e,this.config.botToken,{retry:this.config.retry}),await this.resendTypingIfActive(t)}async stop(){try{await this.bot.stop(),f.info("Telegram bot stopped"),await new Promise(t=>setTimeout(t,100))}catch(t){f.warn(`Error stopping Telegram bot: ${t}`)}}async handleIncoming(t,a){const o=String(t.from?.id??"unknown"),s=String(t.chat?.id??"unknown"),r=t.from?.username,c=i(this.tokenDb,o,"telegram",this.config.dmPolicy,this.config.allowFrom);if(!c.authorized)return f.warn(`Unauthorized message from ${o} (@${r})`),void await t.reply(c.reason??"Not authorized.");this.startTypingInterval(s);try{const i=await this.buildIncomingMessage(t,s,o,r),c=await a(i),{textParts:l,mediaEntries:d}=n(c);for(const i of d)try{const n=new e(i.path);i.asVoice?await t.replyWithVoice(n):await t.replyWithAudio(n)}catch(t){f.error(`Failed to send audio: ${t}`)}const h=l.join("\n").trim();h&&await this.sendChunked(t,h),await this.resendTypingIfActive(s)}catch(e){f.error(`Error handling message from ${o}: ${e}`),await t.reply("An error occurred while processing your message.").catch(()=>{})}}async buildIncomingMessage(t,e,i,n){const a=t.message,o=[];let s=a.text??a.caption??void 0;if(a.photo&&a.photo.length>0){const e=a.photo[a.photo.length-1];o.push({type:"image",mimeType:"image/jpeg",fileSize:e.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,e.file_id)})}if(a.voice&&o.push({type:"voice",mimeType:a.voice.mime_type??"audio/ogg",duration:a.voice.duration,fileSize:a.voice.file_size,getBuffer:()=>this.downloadFile(t,a.voice.file_id)}),a.audio&&o.push({type:"audio",mimeType:a.audio.mime_type??"audio/mpeg",fileName:a.audio.file_name,duration:a.audio.duration,fileSize:a.audio.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,a.audio.file_id)}),a.document&&o.push({type:"document",mimeType:a.document.mime_type,fileName:a.document.file_name,fileSize:a.document.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,a.document.file_id)}),a.video&&o.push({type:"video",mimeType:a.video.mime_type??"video/mp4",fileName:a.video.file_name,duration:a.video.duration,fileSize:a.video.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,a.video.file_id)}),a.video_note&&o.push({type:"video_note",mimeType:"video/mp4",duration:a.video_note.duration,fileSize:a.video_note.file_size,getBuffer:()=>this.downloadFile(t,a.video_note.file_id)}),a.sticker){if(this.config.actions?.sticker)try{g({fileId:a.sticker.file_id,fileUniqueId:a.sticker.file_unique_id,emoji:a.sticker.emoji,setName:a.sticker.set_name,description:a.sticker.emoji?`${a.sticker.emoji} sticker${a.sticker.set_name?` from ${a.sticker.set_name}`:""}`:a.sticker.set_name??"sticker"})}catch(t){f.warn(`Failed to cache sticker: ${t}`)}o.push({type:"sticker",mimeType:a.sticker.is_animated?"application/x-tgsticker":a.sticker.is_video?"video/webm":"image/webp",metadata:{emoji:a.sticker.emoji,setName:a.sticker.set_name},getBuffer:()=>this.downloadFile(t,a.sticker.file_id)})}return a.location&&o.push({type:"location",metadata:{latitude:a.location.latitude,longitude:a.location.longitude},getBuffer:async()=>Buffer.alloc(0)}),a.contact&&o.push({type:"contact",metadata:{phoneNumber:a.contact.phone_number,firstName:a.contact.first_name,lastName:a.contact.last_name,userId:a.contact.user_id},getBuffer:async()=>Buffer.alloc(0)}),{chatId:e,userId:i,channelName:"telegram",text:s,attachments:o,username:n,rawContext:t}}async downloadFile(t,e){const i=await t.api.getFile(e),n=`https://api.telegram.org/file/bot${this.config.botToken}/${i.file_path}`,a=await fetch(n);if(!a.ok)throw new Error(`Failed to download file: ${a.statusText}`);return Buffer.from(await a.arrayBuffer())}async sendChunked(t,e){const i=a(e,4096);for(const n of i)try{await t.reply(n,{parse_mode:"HTML"})}catch{const i=p(e,4096);for(const e of i)await t.reply(e);break}}}function p(t,e){if(t.length<=e)return[t];const i=[];let n=t;for(;n.length>0;){if(n.length<=e){i.push(n);break}let t=n.lastIndexOf("\n",e);t<=0&&(t=e),i.push(n.slice(0,t)),n=n.slice(t).trimStart()}return i}
1
+ import{Bot as t,InputFile as e}from"grammy";import{validateChannelUser as i}from"../../../auth/auth-middleware.js";import{parseMediaLines as n}from"../../../utils/media-response.js";import{markdownToTelegramHtmlChunks as a}from"../../../utils/telegram-format.js";import{createLogger as o}from"../../../utils/logger.js";import{sendMessageTelegram as s}from"./send.js";import{reactMessageTelegram as r,resolveReactionLevel as c}from"./reactions.js";import{editMessageTelegram as l,deleteMessageTelegram as h}from"./edit-delete.js";import{sendStickerTelegram as d,cacheSticker as g}from"./stickers.js";import{validateButtonsForChatId as f}from"./inline-buttons.js";const m=o("Telegram");export class TelegramChannel{name="telegram";bot;config;accountId;tokenDb;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(e,i,n,a=!0){this.config=e,this.accountId=i,this.tokenDb=n,this.inflightTyping=a,this.bot=new t(e.botToken)}async start(t){this.bot.on("message",e=>{this.handleIncoming(e,t)}),this.bot.on("callback_query:data",e=>{const i=e.callbackQuery.data,n=String(e.from?.id??"unknown"),a=String(e.chat?.id??e.callbackQuery.message?.chat?.id??"unknown"),o=e.from?.username;e.answerCallbackQuery().catch(()=>{}),this.startTypingInterval(a);t({chatId:a,userId:n,channelName:"telegram",text:i,attachments:[],username:o,rawContext:e}).then(async t=>{t&&t.trim()&&await this.sendText(a,t)}).catch(t=>{m.error(`Error handling callback from ${n}: ${t}`),this.stopTypingInterval(a)})}),m.info("Starting Telegram bot..."),this.bot.start({onStart:t=>{m.info(`Telegram bot started: @${t.username}`)}}).catch(t=>{String(t).includes("Aborted delay")?m.debug("Telegram polling stopped"):m.error(`Telegram bot polling error: ${t}`)})}async sendText(t,e){try{await s(t,e,{token:this.config.botToken,accountId:this.accountId,textChunkLimit:this.config.textChunkLimit,linkPreview:this.config.linkPreview,retry:this.config.retry})}catch(t){throw m.error(`Failed to send message: ${t}`),t}await this.resendTypingIfActive(t)}async setTyping(t){this.typingIntervals.has(t)?await this.bot.api.sendChatAction(t,"typing").catch(()=>{}):this.startTypingInterval(t)}async resendTypingIfActive(t){this.typingIntervals.has(t)&&await this.bot.api.sendChatAction(t,"typing").catch(()=>{})}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t))}async releaseTyping(t){this.stopTypingInterval(t)}startTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.bot.api.sendChatAction(t,"typing").catch(()=>{});const i=setInterval(()=>{const e=this.inflightCount.get(t)??0;!this.inflightTyping||e>0?this.bot.api.sendChatAction(t,"typing").catch(()=>{}):(clearInterval(i),this.typingIntervals.delete(t))},4e3);this.typingIntervals.set(t,i)}stopTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??1)-1;return void(e>0?this.inflightCount.set(t,e):this.inflightCount.delete(t))}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t))}async sendButtons(t,e,i){const n=i.map(t=>t.map(t=>({text:t.text,callback_data:t.callbackData??t.text})));try{f(n,this.config,t)}catch(i){return m.warn(`Button validation failed: ${i}, sending without buttons`),void await this.sendText(t,e)}await s(t,e,{token:this.config.botToken,accountId:this.accountId,buttons:n,textChunkLimit:this.config.textChunkLimit,linkPreview:this.config.linkPreview,retry:this.config.retry}),await this.resendTypingIfActive(t)}async sendAudio(t,i,n){const a=new e(i);n?await this.bot.api.sendVoice(t,a):await this.bot.api.sendAudio(t,a),await this.resendTypingIfActive(t)}async reactMessage(t,e,i,n){const{level:a}=c(this.config);"off"!==a?await r(t,e,i,this.config.botToken,{remove:n}):m.debug("Reactions disabled for this account")}async editMessage(t,e,i,n){const a=n?.map(t=>({text:t.text,callback_data:t.callbackData??t.text}));await l(t,e,i,this.config.botToken,{buttons:a?[a]:void 0,linkPreview:this.config.linkPreview})}async deleteMessage(t,e){await h(t,e,this.config.botToken,this.config.retry)}async sendSticker(t,e){await d(t,e,this.config.botToken,{retry:this.config.retry}),await this.resendTypingIfActive(t)}async stop(){for(const[,t]of this.typingIntervals)clearInterval(t);this.typingIntervals.clear(),this.inflightCount.clear();try{await this.bot.stop(),m.info("Telegram bot stopped"),await new Promise(t=>setTimeout(t,100))}catch(t){m.warn(`Error stopping Telegram bot: ${t}`)}}async handleIncoming(t,a){const o=String(t.from?.id??"unknown"),s=String(t.chat?.id??"unknown"),r=t.from?.username,c=i(this.tokenDb,o,"telegram",this.config.dmPolicy,this.config.allowFrom);if(!c.authorized)return m.warn(`Unauthorized message from ${o} (@${r})`),void await t.reply(c.reason??"Not authorized.");this.startTypingInterval(s);try{const i=await this.buildIncomingMessage(t,s,o,r),c=await a(i),{textParts:l,mediaEntries:h}=n(c);for(const i of h)try{const n=new e(i.path);i.asVoice?await t.replyWithVoice(n):await t.replyWithAudio(n)}catch(t){m.error(`Failed to send audio: ${t}`)}const d=l.join("\n").trim();d&&await this.sendChunked(t,d),await this.resendTypingIfActive(s)}catch(e){m.error(`Error handling message from ${o}: ${e}`),this.stopTypingInterval(s),await t.reply("An error occurred while processing your message.").catch(()=>{})}}async buildIncomingMessage(t,e,i,n){const a=t.message,o=[];let s=a.text??a.caption??void 0;if(a.photo&&a.photo.length>0){const e=a.photo[a.photo.length-1];o.push({type:"image",mimeType:"image/jpeg",fileSize:e.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,e.file_id)})}if(a.voice&&o.push({type:"voice",mimeType:a.voice.mime_type??"audio/ogg",duration:a.voice.duration,fileSize:a.voice.file_size,getBuffer:()=>this.downloadFile(t,a.voice.file_id)}),a.audio&&o.push({type:"audio",mimeType:a.audio.mime_type??"audio/mpeg",fileName:a.audio.file_name,duration:a.audio.duration,fileSize:a.audio.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,a.audio.file_id)}),a.document&&o.push({type:"document",mimeType:a.document.mime_type,fileName:a.document.file_name,fileSize:a.document.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,a.document.file_id)}),a.video&&o.push({type:"video",mimeType:a.video.mime_type??"video/mp4",fileName:a.video.file_name,duration:a.video.duration,fileSize:a.video.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(t,a.video.file_id)}),a.video_note&&o.push({type:"video_note",mimeType:"video/mp4",duration:a.video_note.duration,fileSize:a.video_note.file_size,getBuffer:()=>this.downloadFile(t,a.video_note.file_id)}),a.sticker){if(this.config.actions?.sticker)try{g({fileId:a.sticker.file_id,fileUniqueId:a.sticker.file_unique_id,emoji:a.sticker.emoji,setName:a.sticker.set_name,description:a.sticker.emoji?`${a.sticker.emoji} sticker${a.sticker.set_name?` from ${a.sticker.set_name}`:""}`:a.sticker.set_name??"sticker"})}catch(t){m.warn(`Failed to cache sticker: ${t}`)}o.push({type:"sticker",mimeType:a.sticker.is_animated?"application/x-tgsticker":a.sticker.is_video?"video/webm":"image/webp",metadata:{emoji:a.sticker.emoji,setName:a.sticker.set_name},getBuffer:()=>this.downloadFile(t,a.sticker.file_id)})}return a.location&&o.push({type:"location",metadata:{latitude:a.location.latitude,longitude:a.location.longitude},getBuffer:async()=>Buffer.alloc(0)}),a.contact&&o.push({type:"contact",metadata:{phoneNumber:a.contact.phone_number,firstName:a.contact.first_name,lastName:a.contact.last_name,userId:a.contact.user_id},getBuffer:async()=>Buffer.alloc(0)}),{chatId:e,userId:i,channelName:"telegram",text:s,attachments:o,username:n,rawContext:t}}async downloadFile(t,e){const i=await t.api.getFile(e),n=`https://api.telegram.org/file/bot${this.config.botToken}/${i.file_path}`,a=await fetch(n);if(!a.ok)throw new Error(`Failed to download file: ${a.statusText}`);return Buffer.from(await a.arrayBuffer())}async sendChunked(t,e){const i=a(e,4096);for(const n of i)try{await t.reply(n,{parse_mode:"HTML"})}catch{const i=p(e,4096);for(const e of i)await t.reply(e);break}}}function p(t,e){if(t.length<=e)return[t];const i=[];let n=t;for(;n.length>0;){if(n.length<=e){i.push(n);break}let t=n.lastIndexOf("\n",e);t<=0&&(t=e),i.push(n.slice(0,t)),n=n.slice(t).trimStart()}return i}
@@ -1 +1 @@
1
- import{DisconnectReason as t,fetchLatestBaileysVersion as e,makeCacheableSignalKeyStore as s,makeWASocket as n,useMultiFileAuthState as i}from"@whiskeysockets/baileys";import{mkdirSync as o,existsSync as a,readFileSync as c}from"node:fs";import{resolve as r}from"node:path";import{renderQrPngBase64 as h}from"./qr-image.js";import{convertMarkdownTables as l}from"../../utils/markdown/tables.js";import{chunkText as p}from"../../utils/chunk.js";import{createLogger as g}from"../../utils/logger.js";const d=g("WhatsApp");export class WhatsAppChannel{name="whatsapp";sock=null;config;qrCallback=null;connected=!1;stopping=!1;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(t,e=!0){this.config=t,this.inflightTyping=e}setQrCallback(t){this.qrCallback=t}isConnected(){return this.connected}async start(t){const e=r(this.config.authDir);a(e)||o(e,{recursive:!0}),await this.connect(e,t)}async connect(o,a){const{state:c,saveCreds:r}=await i(o),{version:l}=await e(),p={level:"silent",trace:()=>{},debug:()=>{},info:()=>{},warn:()=>{},error:()=>{},fatal:()=>{},child:()=>p};this.sock=n({auth:{creds:c.creds,keys:s(c.keys,p)},version:l,logger:p,printQRInTerminal:!1,browser:["GrabMeABeer","Web","1.0"],syncFullHistory:!1,markOnlineOnConnect:!1}),this.sock.ev.on("creds.update",r),this.sock.ev.on("connection.update",async e=>{try{const{connection:s,lastDisconnect:n,qr:i}=e;if(i){d.info("QR code received, rendering...");const t=`data:image/png;base64,${await h(i)}`;this.qrCallback?.(t,!1)}if("open"===s&&(this.connected=!0,d.info("WhatsApp connected"),this.qrCallback?.(null,!0)),"close"===s){this.connected=!1;const e=n?.error?.output?.statusCode??n?.error?.status;e===t.loggedOut?(d.warn("WhatsApp session logged out. Re-scan QR via Nostromo."),this.qrCallback?.(null,!1,"Session logged out. Please re-scan QR code.")):this.stopping||(d.info(`WhatsApp disconnected (code ${e}), reconnecting...`),setTimeout(()=>this.connect(o,a),3e3))}}catch(t){d.error(`connection.update handler error: ${t}`)}}),this.sock.ws&&"function"==typeof this.sock.ws.on&&this.sock.ws.on("error",t=>{d.error(`WebSocket error: ${t.message}`)}),this.sock.ev.on("messages.upsert",({messages:t,type:e})=>{if("notify"===e)for(const e of t){if(!e.message||e.key.fromMe)continue;const t=e.key.remoteJid;if(!t)continue;const s=t.replace(/@s\.whatsapp\.net$/,""),n=e.message.conversation??e.message.extendedTextMessage?.text??void 0;if(!this.checkAccess(s)){d.warn(`Unauthorized message from ${s}`),this.sock?.sendMessage(t,{text:"Not authorized."});continue}if(!n)continue;const i={chatId:t,userId:s,channelName:"whatsapp",text:n,attachments:[],username:e.pushName??void 0};this.handleIncoming(i,t,s,a)}})}async handleIncoming(t,e,s,n){this.startTypingInterval(e);try{const s=await n(t),i=l(s,"code"),o=p(i,4e3);for(const t of o)await(this.sock?.sendMessage(e,{text:t}));await this.resendTypingIfActive(e)}catch(t){d.error(`Error handling message from ${s}: ${t}`)}}checkAccess(t){const e=this.config.dmPolicy||"allowlist";if("open"===e)return!0;if("allowlist"===e){return(this.config.allowFrom??[]).some(e=>String(e)===t)}return!0}async sendText(t,e){if(!this.sock)return;const s=l(e,"code"),n=p(s,4e3);for(const e of n)await this.sock.sendMessage(t,{text:e});await this.resendTypingIfActive(t)}async setTyping(t){this.sock&&(this.typingIntervals.has(t)?await this.sock.sendPresenceUpdate("composing",t).catch(()=>{}):this.startTypingInterval(t))}async resendTypingIfActive(t){this.typingIntervals.has(t)&&await(this.sock?.sendPresenceUpdate("composing",t).catch(()=>{}))}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{})}async releaseTyping(t){this.stopTypingInterval(t)}startTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("composing",t).catch(()=>{});const s=setInterval(()=>{const e=this.inflightCount.get(t)??0;!this.inflightTyping||e>0?this.sock?.sendPresenceUpdate("composing",t).catch(()=>{}):(clearInterval(s),this.typingIntervals.delete(t),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??1)-1;return void(e>0?this.inflightCount.set(t,e):this.inflightCount.delete(t))}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{})}async sendAudio(t,e,s){if(!this.sock)return;const n=c(e);s?await this.sock.sendMessage(t,{audio:n,mimetype:"audio/ogg; codecs=opus",ptt:!0}):await this.sock.sendMessage(t,{audio:n,mimetype:"audio/mpeg"}),await this.resendTypingIfActive(t)}async stop(){this.stopping=!0;try{this.sock?.ws?.close()}catch{}this.sock=null,this.connected=!1,d.info("WhatsApp stopped")}}
1
+ import{DisconnectReason as t,fetchLatestBaileysVersion as e,makeCacheableSignalKeyStore as s,makeWASocket as n,useMultiFileAuthState as i}from"@whiskeysockets/baileys";import{mkdirSync as o,existsSync as a,readFileSync as c}from"node:fs";import{resolve as r}from"node:path";import{renderQrPngBase64 as h}from"./qr-image.js";import{convertMarkdownTables as l}from"../../utils/markdown/tables.js";import{chunkText as p}from"../../utils/chunk.js";import{createLogger as g}from"../../utils/logger.js";const d=g("WhatsApp");export class WhatsAppChannel{name="whatsapp";sock=null;config;qrCallback=null;connected=!1;stopping=!1;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(t,e=!0){this.config=t,this.inflightTyping=e}setQrCallback(t){this.qrCallback=t}isConnected(){return this.connected}async start(t){const e=r(this.config.authDir);a(e)||o(e,{recursive:!0}),await this.connect(e,t)}async connect(o,a){const{state:c,saveCreds:r}=await i(o),{version:l}=await e(),p={level:"silent",trace:()=>{},debug:()=>{},info:()=>{},warn:()=>{},error:()=>{},fatal:()=>{},child:()=>p};this.sock=n({auth:{creds:c.creds,keys:s(c.keys,p)},version:l,logger:p,printQRInTerminal:!1,browser:["GrabMeABeer","Web","1.0"],syncFullHistory:!1,markOnlineOnConnect:!1}),this.sock.ev.on("creds.update",r),this.sock.ev.on("connection.update",async e=>{try{const{connection:s,lastDisconnect:n,qr:i}=e;if(i){d.info("QR code received, rendering...");const t=`data:image/png;base64,${await h(i)}`;this.qrCallback?.(t,!1)}if("open"===s&&(this.connected=!0,d.info("WhatsApp connected"),this.qrCallback?.(null,!0)),"close"===s){this.connected=!1;const e=n?.error?.output?.statusCode??n?.error?.status;e===t.loggedOut?(d.warn("WhatsApp session logged out. Re-scan QR via Nostromo."),this.qrCallback?.(null,!1,"Session logged out. Please re-scan QR code.")):this.stopping||(d.info(`WhatsApp disconnected (code ${e}), reconnecting...`),setTimeout(()=>this.connect(o,a),3e3))}}catch(t){d.error(`connection.update handler error: ${t}`)}}),this.sock.ws&&"function"==typeof this.sock.ws.on&&this.sock.ws.on("error",t=>{d.error(`WebSocket error: ${t.message}`)}),this.sock.ev.on("messages.upsert",({messages:t,type:e})=>{if("notify"===e)for(const e of t){if(!e.message||e.key.fromMe)continue;const t=e.key.remoteJid;if(!t)continue;const s=t.replace(/@s\.whatsapp\.net$/,""),n=e.message.conversation??e.message.extendedTextMessage?.text??void 0;if(!this.checkAccess(s)){d.warn(`Unauthorized message from ${s}`),this.sock?.sendMessage(t,{text:"Not authorized."});continue}if(!n)continue;const i={chatId:t,userId:s,channelName:"whatsapp",text:n,attachments:[],username:e.pushName??void 0};this.handleIncoming(i,t,s,a)}})}async handleIncoming(t,e,s,n){this.startTypingInterval(e);try{const s=await n(t),i=l(s,"code"),o=p(i,4e3);for(const t of o)await(this.sock?.sendMessage(e,{text:t}));await this.resendTypingIfActive(e)}catch(t){d.error(`Error handling message from ${s}: ${t}`),this.stopTypingInterval(e)}}checkAccess(t){const e=this.config.dmPolicy||"allowlist";if("open"===e)return!0;if("allowlist"===e){return(this.config.allowFrom??[]).some(e=>String(e)===t)}return!0}async sendText(t,e){if(!this.sock)return;const s=l(e,"code"),n=p(s,4e3);for(const e of n)await this.sock.sendMessage(t,{text:e});await this.resendTypingIfActive(t)}async setTyping(t){this.sock&&(this.typingIntervals.has(t)?await this.sock.sendPresenceUpdate("composing",t).catch(()=>{}):this.startTypingInterval(t))}async resendTypingIfActive(t){this.typingIntervals.has(t)&&await(this.sock?.sendPresenceUpdate("composing",t).catch(()=>{}))}async clearTyping(t){this.inflightCount.delete(t);const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{})}async releaseTyping(t){this.stopTypingInterval(t)}startTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??0)+1;if(this.inflightCount.set(t,e),e>1)return}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("composing",t).catch(()=>{});const s=setInterval(()=>{const e=this.inflightCount.get(t)??0;!this.inflightTyping||e>0?this.sock?.sendPresenceUpdate("composing",t).catch(()=>{}):(clearInterval(s),this.typingIntervals.delete(t),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{}))},4e3);this.typingIntervals.set(t,s)}stopTypingInterval(t){if(this.inflightTyping){const e=(this.inflightCount.get(t)??1)-1;return void(e>0?this.inflightCount.set(t,e):this.inflightCount.delete(t))}const e=this.typingIntervals.get(t);e&&(clearInterval(e),this.typingIntervals.delete(t)),this.sock?.sendPresenceUpdate("paused",t).catch(()=>{})}async sendAudio(t,e,s){if(!this.sock)return;const n=c(e);s?await this.sock.sendMessage(t,{audio:n,mimetype:"audio/ogg; codecs=opus",ptt:!0}):await this.sock.sendMessage(t,{audio:n,mimetype:"audio/mpeg"}),await this.resendTypingIfActive(t)}async stop(){this.stopping=!0;for(const[,t]of this.typingIntervals)clearInterval(t);this.typingIntervals.clear(),this.inflightCount.clear();try{this.sock?.ws?.close()}catch{}this.sock=null,this.connected=!1,d.info("WhatsApp stopped")}}
@@ -1 +1 @@
1
- import{createLogger as e}from"../utils/logger.js";const t=e("MessageProcessor");export class MessageProcessor{stt;saveFn;constructor(e,t){this.stt=e,this.saveFn=t}async process(e){const a=`${e.channelName}:${e.chatId}`,s=[],i=[];e.text&&s.push({type:"text",text:e.text});for(const o of e.attachments)try{await this.processAttachment(o,a,s,i)}catch(e){t.error(`Error processing attachment type=${o.type}: ${e}`),s.push({type:"text",text:`[Failed to process ${o.type} attachment: ${e}]`})}return 0===s.length&&s.push({type:"text",text:"[Empty message]"}),{sessionKey:a,contentBlocks:s,savedFiles:i}}async saveFile(e,t,a){return this.saveFn?this.saveFn(e,t,a):null}async processAttachment(e,t,a,s){switch(e.type){case"image":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"image.jpg");o&&s.push(o);const c=i.toString("base64"),n=e.mimeType??"image/jpeg";a.push({type:"image",imageBase64:c,imageMimeType:n}),e.caption&&a.push({type:"text",text:e.caption});break}case"voice":case"video_note":{const i=await e.getBuffer();if(this.stt){const t=await this.stt.transcribe(i,e.mimeType??"audio/ogg");a.push({type:"text",text:`[Voice message]: ${t}`})}else{const o=await this.saveFile(t,i,e.fileName??"voice.ogg");o?(s.push(o),a.push({type:"text",text:`[Voice message saved to: ${o}] (STT not configured)`})):a.push({type:"text",text:"[Voice message received] (STT not configured, storage not available)"})}break}case"audio":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"audio.mp3");o&&s.push(o);const c=o?`[Audio file saved to: ${o}]`:"[Audio file received]";if(this.stt)try{const t=await this.stt.transcribe(i,e.mimeType??"audio/mpeg");a.push({type:"text",text:`${c}\n[Transcription]: ${t}`})}catch{a.push({type:"text",text:c})}else a.push({type:"text",text:c});break}case"document":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"document");o&&s.push(o),a.push({type:"text",text:`[Document${o?` saved to: ${o}`:" received"}]${e.caption?`\nCaption: ${e.caption}`:""}`});break}case"video":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"video.mp4");o&&s.push(o),a.push({type:"text",text:`[Video${o?` saved to: ${o}`:" received"}]${e.caption?`\nCaption: ${e.caption}`:""}`});break}case"sticker":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"sticker.webp");o&&s.push(o),a.push({type:"text",text:`[Sticker${o?` saved to: ${o}`:" received"}]`});break}case"location":{const t=e.metadata??{};a.push({type:"text",text:`[Location: lat=${t.latitude}, lon=${t.longitude}]`});break}case"contact":{const t=e.metadata??{},s=[t.firstName&&`Name: ${t.firstName}`,t.lastName&&` ${t.lastName}`,t.phoneNumber&&`Phone: ${t.phoneNumber}`].filter(Boolean);a.push({type:"text",text:`[Contact: ${s.join(", ")}]`});break}}}}
1
+ import{createLogger as e}from"../utils/logger.js";const t=e("MessageProcessor");export class MessageProcessor{stt;saveFn;constructor(e,t){this.stt=e,this.saveFn=t}async process(e){const a=`${e.channelName}:${e.chatId}`,s=[],i=[];e.text&&s.push({type:"text",text:e.text});for(const o of e.attachments)try{await this.processAttachment(o,a,s,i)}catch(e){t.error(`Error processing attachment type=${o.type}: ${e}`),s.push({type:"text",text:`[Failed to process ${o.type} attachment: ${e}]`})}return 0===s.length&&s.push({type:"text",text:"[Empty message]"}),{sessionKey:a,contentBlocks:s,savedFiles:i}}async saveFile(e,t,a){return this.saveFn?this.saveFn(e,t,a):null}async processAttachment(e,t,a,s){switch(e.type){case"image":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"image.jpg");o&&s.push(o);const c=i.toString("base64"),n=e.mimeType??"image/jpeg";a.push({type:"image",imageBase64:c,imageMimeType:n}),e.caption&&a.push({type:"text",text:e.caption});break}case"voice":case"video_note":{const i=await e.getBuffer();if(this.stt){const t=await this.stt.transcribe(i,e.mimeType??"audio/ogg");a.push({type:"text",text:`[Voice message]: ${t}`})}else{const o=await this.saveFile(t,i,e.fileName??"voice.ogg");o?(s.push(o),a.push({type:"text",text:`[Voice message saved to: ${o}] (STT not configured)`})):a.push({type:"text",text:"[Voice message received] (STT not configured, storage not available)"})}break}case"audio":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"audio.mp3");o&&s.push(o);const c=o?`[Audio file saved to: ${o}]`:"[Audio file received]";if(this.stt)try{const t=await this.stt.transcribe(i,e.mimeType??"audio/mpeg");a.push({type:"text",text:`${c}\n[Transcription]: ${t}`})}catch{a.push({type:"text",text:c})}else a.push({type:"text",text:c});break}case"document":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"document");o&&s.push(o),a.push({type:"text",text:`[Document${o?` saved to: ${o}`:" received"}]${e.caption?`\nCaption: ${e.caption}`:""}`});break}case"video":{const i=await e.getBuffer(),o=await this.saveFile(t,i,e.fileName??"video.mp4");o&&s.push(o),a.push({type:"text",text:`[Video${o?` saved to: ${o}`:" received"}]${e.caption?`\nCaption: ${e.caption}`:""}`});break}case"sticker":{const i=await e.getBuffer(),o=function(e){switch(e){case"application/x-tgsticker":return".tgs";case"video/webm":return".webm";default:return".webp"}}(e.mimeType),c=await this.saveFile(t,i,e.fileName??`sticker${o}`);c&&s.push(c);const n=e.metadata?.emoji?` (${e.metadata.emoji})`:"";a.push({type:"text",text:`[Sticker${n}${c?` saved to: ${c}`:" received"}]`});break}case"location":{const t=e.metadata??{};a.push({type:"text",text:`[Location: lat=${t.latitude}, lon=${t.longitude}]`});break}case"contact":{const t=e.metadata??{},s=[t.firstName&&`Name: ${t.firstName}`,t.lastName&&` ${t.lastName}`,t.phoneNumber&&`Phone: ${t.phoneNumber}`].filter(Boolean);a.push({type:"text",text:`[Contact: ${s.join(", ")}]`});break}}}}
@@ -1 +1 @@
1
- import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as d}from"./pi-context-compactor.js";import{randomUUID as u}from"node:crypto";let m=null;async function p(){if(!m)try{const e=await import("@mariozechner/pi-ai");m={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return m}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(m,h,b){const{prompt:_,options:k}=m;let T=new AbortController,v=!1,P=!1,$=h.modelId,C=h.provider;const I=k.resume||u();let R=g.get(I);if(!R){let e="";if("string"==typeof k.systemPrompt)e=k.systemPrompt;else if(k.systemPrompt&&"object"==typeof k.systemPrompt){const t=k.systemPrompt.preset,o=k.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}R={id:I,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(I,R)}R.lastAccessTime=Date.now();const M=b??new s;if(b||(M.registerTools(n),k.mcpServers&&Object.keys(k.mcpServers).length>0&&a(k).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&M.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),k.canUseTool&&M.setPermissionChecker(k.canUseTool),!b){const e=k.mcpServers??{};M.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,k.cwd,I);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*D(s){if(P)return;let n=R.systemPrompt;const r=i(k.cwd||"");if(r&&(n+="\n\n"+r),R.messages.length>0){const e=c(R.messages,n,{...d,contextWindowTokens:h.contextWindowTokens??128e3},R.lastCompactedIndex,R.rollingContext);if(e.phase>0&&(R.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${R.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await p();if(t){const o=h.summarizationProvider||C,s=h.summarizationModelId||$,n=t.getModel(o,s),r=t.stream(n,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of r);const a=await r.result(),i=a.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");i&&(R.rollingContext=i,l(R.messages,i,d.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${R.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}R.lastCompactedIndex=R.messages.length}const a=e(s);R.messages.push(a),R.lastAccessTime=Date.now();const u=k.maxTurns??25;let m=0,g="",b="end_turn",_={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const I=Date.now();for(;m<u&&!P&&!v;){m++;const e=M.getFilteredTools(k.allowedTools,k.disallowedTools);let s,r;try{s=await p()}catch(e){return void(yield y(R,"error_during_execution",g,b,$,I,_,[`${e}`]))}try{r=s.getModel(C,$)}catch(e){return void(yield y(R,"error_during_execution",g,b,$,I,_,[`Model not found: ${C}/${$}. ${e}`]))}const a={systemPrompt:n,messages:R.messages.map(x),tools:e.length>0?e:void 0},i={signal:T.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if(P||v||T.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||T.signal.aborted?(v=!1,void(yield y(R,"error_during_execution",g,b,$,I,_,["aborted"]))):void(yield y(R,"error_during_execution",g,b,$,I,_,[`${e}`]))}w(_,c.usage),b=f(c.stopReason);const l=t(c);yield l,R.messages.push(c);const d=o(l);if(d&&(g=d),"toolUse"!==c.stopReason)break;const u=c.content.filter(e=>"toolCall"===e.type);if(0===u.length)break;for(const e of u){if(P||v||T.signal.aborted)break;const t=await M.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};R.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await M.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};R.messages.push(a)}if(P||v||T.signal.aborted)return v=!1,void(yield y(R,"error_during_execution",g,b,$,I,_,["aborted"]))}m>=u&&!P?yield y(R,"error_max_turns",g,b,$,I,_):(v=!1,yield y(R,"success",g,b,$,I,_))}const W=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:I};try{for await(const e of _){if(P)break;(v||T.signal.aborted)&&(v=!1,T=new AbortController);for await(const t of D(e)){if(P)break;yield t}}}catch(e){P||(yield{type:"result",subtype:"error_during_execution",session_id:I,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>W,async interrupt(){v=!0,T.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,$=o}else $=e},close(){P=!0,T.abort(),g.delete(I)},async supportedModels(){try{const e=await p();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
1
+ import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as u}from"./pi-context-compactor.js";import{randomUUID as d}from"node:crypto";let p=null;async function m(){if(!p)try{const e=await import("@mariozechner/pi-ai");p={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return p}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(p,h,b){const{prompt:k,options:T}=p;let _=new AbortController,v=!1,$=!1,P=h.modelId,C=h.provider;const R=T.resume||d();let W=g.get(R);if(!W){let e="";if("string"==typeof T.systemPrompt)e=T.systemPrompt;else if(T.systemPrompt&&"object"==typeof T.systemPrompt){const t=T.systemPrompt.preset,o=T.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}W={id:R,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(R,W)}W.lastAccessTime=Date.now();const I=b??new s;if(b||(I.registerTools(n),T.mcpServers&&Object.keys(T.mcpServers).length>0&&a(T).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&I.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),T.canUseTool&&I.setPermissionChecker(T.canUseTool),!b){const e=T.mcpServers??{};I.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,T.cwd,R);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*M(s){if($)return;let n=W.systemPrompt;const r=i(T.cwd||"");if(r&&(n+="\n\n"+r),W.messages.length>0){const e=c(W.messages,n,{...u,contextWindowTokens:h.contextWindowTokens??128e3},W.lastCompactedIndex,W.rollingContext);if(e.phase>0&&(W.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${W.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await m();if(t){const o=h.summarizationProvider||C,s=h.summarizationModelId||P,n=t.getModel(o,s),r=t.stream(n,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of r);const a=await r.result(),i=a.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");i&&(W.rollingContext=i,l(W.messages,i,u.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${W.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}W.lastCompactedIndex=W.messages.length}const a=e(s);W.messages.push(a),W.lastAccessTime=Date.now();const d=T.maxTurns??25;let p=0,g="",b="end_turn",k={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const R=Date.now();for(;p<d&&!$&&!v;){p++;const e=I.getFilteredTools(T.allowedTools,T.disallowedTools);let s,r;try{s=await m()}catch(e){return void(yield y(W,"error_during_execution",g,b,P,R,k,[`${e}`]))}try{r=s.getModel(C,P)}catch(e){return void(yield y(W,"error_during_execution",g,b,P,R,k,[`Model not found: ${C}/${P}. ${e}`]))}if(!r&&"openrouter"===C){const e=P.startsWith("anthropic/")?"anthropic-messages":P.startsWith("google/")?"google":"openai-completions";r={id:P,name:P,api:e,provider:"openrouter",baseUrl:"https://openrouter.ai/api/v1",reasoning:!0,input:["text","image"],contextWindow:h.contextWindowTokens||4e5,maxTokens:128e3,cost:{input:0,output:0,cacheRead:0,cacheWrite:0}},console.warn(`[pi-query] Model ${C}/${P} not in pi-ai registry, using synthetic entry (api=${e})`)}const a={systemPrompt:n,messages:W.messages.map(x),tools:e.length>0?e:void 0},i={signal:_.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if($||v||_.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||_.signal.aborted?(v=!1,void(yield y(W,"error_during_execution",g,b,P,R,k,["aborted"]))):void(yield y(W,"error_during_execution",g,b,P,R,k,[`${e}`]))}w(k,c.usage),b=f(c.stopReason);const l=t(c);yield l,W.messages.push(c);const u=o(l);if(u&&(g=u),"toolUse"!==c.stopReason)break;const d=c.content.filter(e=>"toolCall"===e.type);if(0===d.length)break;for(const e of d){if($||v||_.signal.aborted)break;const t=await I.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};W.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await I.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};W.messages.push(a)}if($||v||_.signal.aborted)return v=!1,void(yield y(W,"error_during_execution",g,b,P,R,k,["aborted"]))}p>=d&&!$?yield y(W,"error_max_turns",g,b,P,R,k):(v=!1,yield y(W,"success",g,b,P,R,k))}const D=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:R};try{for await(const e of k){if($)break;(v||_.signal.aborted)&&(v=!1,_=new AbortController);for await(const t of M(e)){if($)break;yield t}}}catch(e){$||(yield{type:"result",subtype:"error_during_execution",session_id:R,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>D,async interrupt(){v=!0,_.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,P=o}else P=e},close(){$=!0,_.abort(),g.delete(R)},async supportedModels(){try{const e=await m();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as m}from"./gateway/channels/whatsapp.js";import{WebChatChannel as l}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as y,loadWorkspaceFiles as S}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as j,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as $}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 B}from"./commands/debugdynamic.js";import{CronService as H}from"./cron/cron-service.js";import{stripHeartbeatToken as L,isHeartbeatContentEffectivelyEmpty as Q}from"./cron/heartbeat-token.js";import{createServerToolsServer as W}from"./tools/server-tools.js";import{createCronToolsServer as z}from"./tools/cron-tools.js";import{createTTSToolsServer as G}from"./tools/tts-tools.js";import{createMemoryToolsServer as q}from"./tools/memory-tools.js";import{createBrowserToolsServer as V}from"./tools/browser-tools.js";import{createPicoToolsServer as J}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as X}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Y}from"./tools/concept-tools.js";import{BrowserService as Z}from"./browser/browser-service.js";import{MemorySearch as ee}from"./memory/memory-search.js";import{ConceptStore as te}from"./memory/concept-store.js";import{stripMediaLines as se}from"./utils/media-response.js";import{loadConfig as ne,loadRawConfig as oe,backupConfig as ie,resolveModelEntry as re,modelRefName as ae}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as he}from"./utils/logger.js";import{SessionErrorHandler as ge}from"./agent/session-error-handler.js";import{initStickerCache as me}from"./gateway/channels/telegram/stickers.js";const le=he("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),me(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new Z,this.conceptStore=new te(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),m=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,m?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new H({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new ee(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),q(this.memorySearch);le.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return le.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=o(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(Q(n))return le.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=i?"cron":t.channel,a=i?t.name:t.chatId;le.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=L(h,e);if(s)return le.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();le.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g))),await Promise.allSettled(e.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g),await this.channelManager.releaseTyping(t.channel,t.chatId).catch(()=>{});return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??ae(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ae(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new x(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new D(()=>this.agentService.getToolServers())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){le.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new m(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new l),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";le.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&le.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/")&&this.agentService.isBusy(s))return"I'm busy right now. Please resend this request later.";const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=u(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId});le.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,c={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},h=S(this.config.dataDir),g={config:this.config,sessionContext:c,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},m=b(g),l=b({...g,mode:"minimal"});le.debug(`[${s}] System prompt (${m.length} chars): ${this.config.verboseDebugLogs?m:m.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,m,l,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=ge.analyzeError(r.error,r),c=ge.getRecoveryStrategy(a);return le.warn(`[${s}] ${c.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),c.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==c.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(le.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(le.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",se(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(le.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){le.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),le.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{le.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),le.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),le.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),le.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?le.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?le.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):le.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){le.info("Trigger restart requested");const e=ne();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();le.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),le.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){le.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(le.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>le.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){le.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){le.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}le.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){le.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),le.info("Server reconfigured successfully")}async stop(){le.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),le.info("Server stopped")}}
1
+ import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as m}from"./gateway/channels/whatsapp.js";import{WebChatChannel as l}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as y,loadWorkspaceFiles as S}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as j,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as D}from"./commands/help.js";import{McpCommand as $}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as P}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as F}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{DebugA2UICommand as O}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as H}from"./cron/cron-service.js";import{stripHeartbeatToken as L,isHeartbeatContentEffectivelyEmpty as Q}from"./cron/heartbeat-token.js";import{createServerToolsServer as W}from"./tools/server-tools.js";import{createCronToolsServer as z}from"./tools/cron-tools.js";import{createTTSToolsServer as G}from"./tools/tts-tools.js";import{createMemoryToolsServer as q}from"./tools/memory-tools.js";import{createBrowserToolsServer as V}from"./tools/browser-tools.js";import{createPicoToolsServer as J}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as X}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Y}from"./tools/concept-tools.js";import{BrowserService as Z}from"./browser/browser-service.js";import{MemorySearch as ee}from"./memory/memory-search.js";import{ConceptStore as te}from"./memory/concept-store.js";import{stripMediaLines as se}from"./utils/media-response.js";import{loadConfig as ne,loadRawConfig as oe,backupConfig as ie,resolveModelEntry as re,modelRefName as ae}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as he}from"./utils/logger.js";import{SessionErrorHandler as ge}from"./agent/session-error-handler.js";import{initStickerCache as me}from"./gateway/channels/telegram/stickers.js";const le=he("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),me(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new Z,this.conceptStore=new te(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),m=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,m?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new H({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new ee(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK}),q(this.memorySearch);le.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return le.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=o(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(Q(n))return le.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=i?"cron":t.channel,a=i?t.name:t.chatId;le.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=L(h,e);if(s)return le.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();le.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g))),await Promise.allSettled(e.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g),await this.channelManager.releaseTyping(t.channel,t.chatId).catch(()=>{});return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:ae(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ae(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ae(s):void 0),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new x(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new $(()=>this.agentService.getToolServers())),this.commandRegistry.register(new D(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){le.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new m(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}this.webChatChannel||(this.webChatChannel=new l),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";le.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&le.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/")&&this.agentService.isBusy(s))return"I'm busy right now. Please resend this request later.";const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=u(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId});le.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,c={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},h=S(this.config.dataDir),g={config:this.config,sessionContext:c,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},m=b(g),l=b({...g,mode:"minimal"});le.debug(`[${s}] System prompt (${m.length} chars): ${this.config.verboseDebugLogs?m:m.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,m,l,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=ge.analyzeError(r.error,r),c=ge.getRecoveryStrategy(a);return le.warn(`[${s}] ${c.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),c.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==c.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(le.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(le.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",se(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(le.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){le.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),le.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{le.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),le.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),le.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),le.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?le.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?le.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):le.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){le.info("Trigger restart requested");const e=ne();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();le.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),le.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){le.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(le.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>le.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){le.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){le.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}le.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){le.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),le.info("Server reconfigured successfully")}async stop(){le.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),le.info("Server stopped")}}
@@ -1 +1 @@
1
- import{createSdkMcpServer as t,tool as e}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{join as a}from"node:path";import{existsSync as i,readdirSync as s,readFileSync as o,writeFileSync as r,mkdirSync as m,rmSync as c}from"node:fs";import{createLogger as d}from"../utils/logger.js";const l=d("PlasmaClientTools");function p(t){return n.preprocess(t=>{if("string"==typeof t)try{return JSON.parse(t)}catch{return t}return t},t)}function u(t){const e=t.split("\n");let n=null,a=[];const i={};for(const t of e)t.startsWith("---")?(n&&(i[n]=a.join("\n").trim()),n=null,a=[]):t.match(/^(html|css|js|activities):\s*\|?$/)?(n&&(i[n]=a.join("\n").trim()),n=t.split(":")[0],a=[]):n&&a.push(t.replace(/^ /,""));n&&(i[n]=a.join("\n").trim());let s=[];if(i.activities)try{s=i.activities.split(/^(?=- )/m).filter(Boolean).map(t=>{const e={},n=t.match(/id:\s*(.+)/),a=t.match(/type:\s*(.+)/),i=t.match(/context:\s*(.+)/),s=t.match(/dataProvider:\s*(.+)/);if(n&&(e.id=n[1].trim()),a&&(e.type=a[1].trim()),i)try{e.context=JSON.parse(i[1].trim())}catch{}return s&&(e.dataProvider=s[1].trim()),e}).filter(t=>t.id&&t.type)}catch{try{s=JSON.parse(i.activities)}catch{s=[]}}return{html:i.html||"",css:i.css,js:i.js,activities:s}}function h(t){if(!i(t))return[];const e=s(t),n=[];for(const i of e){const e=i.match(/^(\d+)_(.+)\.code$/);e&&n.push({num:parseInt(e[1],10),name:e[2],path:a(t,i)})}return n.sort((t,e)=>t.num-e.num)}export function createPlasmaClientToolsServer(d){const g=a(d.plasmaRootDir,"organisms");return i(g)||m(g,{recursive:!0}),t({name:"plasma-client-tools",version:"2.0.0",tools:[e("plasma_create","Create a new PLASMA organism (UI application).\n\nAn organism is a complete UI app with event sourcing:\n- main.code: Initial version (HTML/CSS/JS/activities in YAML format)\n- Mutations: Incremental updates (JavaScript only)\n- Snapshots: Automatic every 20 mutations for fast loading\n\nNaming convention: a-zA-Z0-9_ only, max 4 words, descriptive of the app.\nExamples: customer_form, sales_dashboard, task_manager, chat_interface\n\nThe organism is saved to .plasma/organisms/{name}/ and can be:\n- Mutated incrementally with plasma_mutate\n- Loaded instantly with plasma_load\n- Optimized with plasma_optimize (LLM-based compaction)\n\nThis enables fast app loading and complete change history.",{name:n.string().describe("Organism name (a-zA-Z0-9_ only, max 4 words, descriptive)"),description:n.string().describe("Description of what this organism does"),html:n.string().describe("HTML content"),css:n.string().optional().describe("CSS styles"),js:n.string().optional().describe("JavaScript code"),activities:p(n.array(n.object({id:n.string(),type:n.enum(["button","input","canvas","custom"]),context:n.any().optional(),dataProvider:n.string().optional().describe("JS expression evaluated at event time. Result is included as 'provided' in the action payload.")}))).describe("Interactive elements"),state:n.any().optional().describe("Initial state data"),tags:p(n.array(n.string())).optional().describe("Tags for categorization")},async t=>{try{!function(t){if(!/^[a-zA-Z0-9_]+$/.test(t))throw new Error("Organism name must contain only a-zA-Z0-9_ characters (BASIC convention)");if(t.split("_").length>4)throw new Error("Organism name must be max 4 words separated by underscores")}(t.name);const n=a(g,t.name);if(i(n))throw new Error(`Organism '${t.name}' already exists. Use plasma_mutate to update it.`);m(n,{recursive:!0});const s={name:t.name,description:t.description,created:(new Date).toISOString(),updated:(new Date).toISOString(),mutations:0,tags:t.tags||[]},o=`---\nname: ${s.name}\ndescription: ${s.description}\ncreated: ${s.created}\nupdated: ${s.updated}\nmutations: ${s.mutations}\n${s.tags&&s.tags.length>0?`tags: [${s.tags.join(", ")}]`:""}\n---\n`;r(a(n,"manifest.yaml"),o,"utf-8");const c={html:t.html,css:t.css,js:t.js,activities:t.activities};return r(a(n,"main.code"),`---\nactivities:\n${(e=c).activities.map(t=>{let e=` - id: ${t.id}\n type: ${t.type}`;return t.context&&(e+=`\n context: ${JSON.stringify(t.context)}`),t.dataProvider&&(e+=`\n dataProvider: ${t.dataProvider}`),e}).join("\n")}\n---\nhtml: |\n ${e.html.split("\n").join("\n ")}\n\n${e.css?`css: |\n ${e.css.split("\n").join("\n ")}\n`:""}\n${e.js?`js: |\n ${e.js.split("\n").join("\n ")}`:""}\n`.trim(),"utf-8"),l.info(`Created organism ${t.name}`),{content:[{type:"text",text:`✅ Organism '${t.name}' created successfully\n\nSaved to: .plasma/organisms/${t.name}/\n\nFiles:\n- manifest.yaml (metadata)\n- main.code (${t.html.length+(t.css?.length||0)+(t.js?.length||0)} bytes)\n\nActivities: ${t.activities.length}\n${t.activities.map(t=>`- #${t.id} (${t.type})`).join("\n")}\n\nUse plasma_load("${t.name}", node_id) to render it on a node.\nUse plasma_mutate("${t.name}", js) to apply incremental updates.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_create failed: ${e}`),{content:[{type:"text",text:`❌ Failed to create organism: ${e}`}],isError:!0}}var e}),e("plasma_mutate","Apply an incremental mutation to an existing organism.\n\nMutations are JavaScript-only updates that modify the existing app.\nEach mutation is saved as N_[descriptive_name].code where N is a progressive number.\n\nExamples:\n- 1_add_validation.code\n- 2_change_theme_blue.code\n- 3_fix_button_layout.code\n\nMutations are applied in sequence when loading the organism.\nSnapshots are created automatically every 20 mutations for performance.\n\nThe mutation is immediately applied to any active rendering via dynamic_ui_update.",{name:n.string().describe("Organism name"),mutation_name:n.string().regex(/^[a-zA-Z0-9_]+$/).describe("Descriptive name for this mutation (a-zA-Z0-9_ only)"),js:n.string().describe("JavaScript code for the mutation (modifies existing state)"),node_id:n.string().optional().describe("Optional: node ID to apply mutation immediately")},async t=>{try{const e=a(g,t.name);if(!i(e))throw new Error(`Organism '${t.name}' not found. Use plasma_list to see available organisms.`);const n=a(e,"manifest.yaml"),s=o(n,"utf-8"),m=s.match(/mutations:\s*(\d+)/),c=(m?parseInt(m[1],10):0)+1,p=`${c}_${t.mutation_name}.code`,u=a(e,p);r(u,t.js,"utf-8");const f=s.replace(/updated:.*/,`updated: ${(new Date).toISOString()}`).replace(/mutations:\s*\d+/,`mutations: ${c}`);r(n,f,"utf-8");let y=!1;c%20==0&&(!function(t,e){const n=a(t,"main.code");if(!i(n))throw new Error("main.code not found");const s=h(t).filter(t=>t.num<=e);let m=`// Snapshot at mutation ${e}\n// Auto-generated: ${(new Date).toISOString()}\n\n`;m+=`// === main.code ===\n${o(n,"utf-8")}\n\n`;for(const t of s)m+=`// === ${t.num}_${t.name}.code ===\n`,m+=o(t.path,"utf-8")+"\n\n";const c=a(t,`snapshot_${e}.code`);r(c,m,"utf-8"),l.info(`Created snapshot at mutation ${e} for ${t}`)}(e,c),y=!0),l.info(`Applied mutation ${c} to organism ${t.name}`);let $=!1;if(t.node_id){const e=d.nodeRegistry.getNode(t.node_id);if(e&&e.capabilities?.includes("plasma")){const n={type:"dynamic_ui_update",js:t.js};d.channel&&(n.channel=d.channel),d.chatId&&(n.chatId=d.chatId),e.ws.send(JSON.stringify(n)),$=!0,l.info(`Applied mutation to node ${t.node_id}`)}}return{content:[{type:"text",text:`✅ Mutation applied to organism '${t.name}'\n\nMutation: ${c}_${t.mutation_name}.code\nTotal mutations: ${c}\n${y?`\n📸 Snapshot created (snapshot_${c}.code) for fast loading`:""}\n${$?`\n✅ Mutation applied to node ${t.node_id}`:""}\n\nThe mutation has been saved and will be applied automatically when loading the organism.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_mutate failed: ${e}`),{content:[{type:"text",text:`❌ Failed to apply mutation: ${e}`}],isError:!0}}}),e("plasma_load","Load a saved PLASMA organism and render it on an ElectroNode.\n\nThis loads the organism from disk with smart snapshot support:\n1. Finds best snapshot (if available) for fast loading\n2. Loads main.code if no snapshot\n3. Applies remaining mutations in sequence\n4. Renders via dynamic_ui_render\n\nUse plasma_list to see available organisms.",{name:n.string().describe("Organism name to load"),node_id:n.string().describe("Target node ID (get from dynamic_ui_list_nodes)")},async t=>{try{const e=a(g,t.name);if(!i(e))throw new Error(`Organism '${t.name}' not found. Use plasma_list to see available organisms.`);const n=a(e,"manifest.yaml"),r=o(n,"utf-8"),m=r.match(/name:\s*(.+)/),c=(r.match(/description:\s*(.+)/),r.match(/mutations:\s*(\d+)/)),p=m?m[1].trim():t.name,f=c?parseInt(c[1],10):0,y=h(e),$=function(t,e){if(!i(t))return null;const n=s(t),o=[];for(const t of n){const e=t.match(/^snapshot_(\d+)\.code$/);e&&o.push(parseInt(e[1],10))}if(0===o.length)return null;const r=e.length>0?e[e.length-1].num:0,m=o.filter(t=>t<=r);if(0===m.length)return null;const c=Math.max(...m);return a(t,`snapshot_${c}.code`)}(e,y);let _,v=0;if($&&i($)){const e=o($,"utf-8"),n=$.match(/snapshot_(\d+)\.code$/);v=n?parseInt(n[1],10):0;const a=e.match(/=== main\.code ===\n([\s\S]+?)(?:\n\/\/ ===|$)/);if(!a)throw new Error("Failed to parse snapshot");_=u(a[1]),l.info(`Loading organism ${t.name} from snapshot_${v}.code`)}else{const n=a(e,"main.code");_=u(o(n,"utf-8")),v=0,l.info(`Loading organism ${t.name} from main.code`)}const x=a(e,"state.json");let w=null;i(x)&&(w=JSON.parse(o(x,"utf-8")));let S=_.js||"";w&&(S=`window.__initialState = ${JSON.stringify(w)};\n\n${S}`);const b=d.nodeRegistry.getNode(t.node_id);if(!b)throw new Error(`Node ${t.node_id} not found or disconnected`);if(!b.capabilities?.includes("plasma"))throw new Error(`Node ${t.node_id} does not support Dynamic UI`);const I={type:"dynamic_ui",html:_.html,css:_.css||"",js:S,activities:_.activities};d.channel&&(I.channel=d.channel),d.chatId&&(I.chatId=d.chatId),b.ws.send(JSON.stringify(I));const j=y.filter(t=>t.num>v);return j.length>0&&setTimeout(()=>{for(const t of j){const e={type:"dynamic_ui_update",js:o(t.path,"utf-8")};d.channel&&(e.channel=d.channel),d.chatId&&(e.chatId=d.chatId),b.ws.send(JSON.stringify(e))}l.info(`Applied ${j.length} mutations to organism ${t.name}`)},100),l.info(`Loaded and rendered organism ${t.name} on node ${t.node_id}`),{content:[{type:"text",text:`✅ Organism '${p}' loaded and rendered\n\nNode: ${b.displayName||t.node_id}\nTotal mutations: ${f}\n${$?`Loaded from: snapshot_${v}.code`:"Loaded from: main.code"}\n${j.length>0?`Applied ${j.length} additional mutations`:""}\nActivities: ${_.activities.length}\n\nThe organism is now active on the node.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_load failed: ${e}`),{content:[{type:"text",text:`❌ Failed to load organism: ${e}`}],isError:!0}}}),e("plasma_list","List all saved PLASMA organisms with their metadata.",{},async()=>{if(!i(g))return{content:[{type:"text",text:"No organisms saved yet. Use plasma_create to create your first organism."}]};const t=s(g,{withFileTypes:!0}).filter(t=>t.isDirectory()&&!t.name.startsWith(".")).map(t=>t.name);if(0===t.length)return{content:[{type:"text",text:"No organisms saved yet. Use plasma_create to create your first organism."}]};const e=t.map(t=>{try{const e=a(g,t,"manifest.yaml");if(i(e)){const n=o(e,"utf-8"),a=n.match(/name:\s*(.+)/),i=n.match(/description:\s*(.+)/),s=n.match(/mutations:\s*(\d+)/),r=n.match(/tags:\s*\[(.+)\]/),m=a?a[1].trim():t,c=i?i[1].trim():"No description",d=s?s[1]:"0";return`- ${m} (${t}) — ${d} mutations${r?` [${r[1]}]`:""}\n ${c}`}return`- ${t} (no manifest)`}catch(e){return`- ${t} (error reading manifest)`}}).join("\n\n");return{content:[{type:"text",text:`${t.length} PLASMA organism(s) available:\n\n${e}`}]}}),e("plasma_delete","Delete a PLASMA organism permanently.",{name:n.string().describe("Organism name to delete")},async t=>{try{const e=a(g,t.name);if(!i(e))throw new Error(`Organism '${t.name}' not found`);return c(e,{recursive:!0,force:!0}),l.info(`Deleted organism ${t.name}`),{content:[{type:"text",text:`✅ Organism '${t.name}' deleted permanently`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_delete failed: ${e}`),{content:[{type:"text",text:`❌ Failed to delete organism: ${e}`}],isError:!0}}}),e("dynamic_ui_query","Execute JavaScript in the DynamicUI surface on a node and return the result.\n\nUse this to read runtime state from the UI — form values, computed data, DOM state, etc.\nThe JS runs in the same context as the rendered PLASMA organism (same iframe/webview).\n\nExamples:\n- Read a form field: \"document.getElementById('name').value\"\n- Read multiple fields: \"({name: document.getElementById('name').value, email: document.getElementById('email').value})\"\n- Read app state: \"JSON.stringify(window.app.records)\"\n- Check element visibility: \"document.getElementById('panel').style.display !== 'none'\"",{node_id:n.string().describe("Target node ID"),js:n.string().describe("JavaScript expression to evaluate. The result is returned to the agent.")},async t=>{try{const e=await d.nodeRegistry.executeCommand(t.node_id,"dynamic_ui.query",{js:t.js},1e4);if(!e.ok)return{content:[{type:"text",text:`❌ dynamic_ui_query failed: ${e.error??"Unknown error"}`}],isError:!0};return{content:[{type:"text",text:`Result: ${"string"==typeof e.result?e.result:JSON.stringify(e.result)}`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`dynamic_ui_query failed: ${e}`),{content:[{type:"text",text:`❌ dynamic_ui_query failed: ${e}`}],isError:!0}}})]})}
1
+ import{createSdkMcpServer as t,tool as e}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{join as a}from"node:path";import{existsSync as i,readdirSync as s,readFileSync as o,writeFileSync as r,mkdirSync as c,rmSync as m}from"node:fs";import{createLogger as d}from"../utils/logger.js";const l=d("PlasmaClientTools");function p(t){return n.preprocess(t=>{if("string"==typeof t)try{return JSON.parse(t)}catch{return t}return t},t)}function u(t){const e=t.split("\n");let n=null,a=[];const i={};for(const t of e)t.startsWith("---")?(n&&(i[n]=a.join("\n").trim()),n=null,a=[]):t.match(/^(html|css|js|activities):\s*\|?$/)?(n&&(i[n]=a.join("\n").trim()),n=t.split(":")[0],a=[]):n&&a.push(t.replace(/^ /,""));n&&(i[n]=a.join("\n").trim());let s=[];if(i.activities)try{s=i.activities.split(/^(?=- )/m).filter(Boolean).map(t=>{const e={},n=t.match(/id:\s*(.+)/),a=t.match(/type:\s*(.+)/),i=t.match(/context:\s*(.+)/),s=t.match(/dataProvider:\s*(.+)/);if(n&&(e.id=n[1].trim()),a&&(e.type=a[1].trim()),i)try{e.context=JSON.parse(i[1].trim())}catch{}return s&&(e.dataProvider=s[1].trim()),e}).filter(t=>t.id&&t.type)}catch{try{s=JSON.parse(i.activities)}catch{s=[]}}return{html:i.html||"",css:i.css,js:i.js,activities:s}}function h(t){if(!i(t))return[];const e=s(t),n=[];for(const i of e){const e=i.match(/^(\d+)_(.+)\.code$/);e&&n.push({num:parseInt(e[1],10),name:e[2],path:a(t,i)})}return n.sort((t,e)=>t.num-e.num)}export function createPlasmaClientToolsServer(d){const g=a(d.plasmaRootDir,"organisms");return i(g)||c(g,{recursive:!0}),t({name:"plasma-client-tools",version:"2.0.0",tools:[e("plasma_create","Create a new PLASMA organism (UI application).\n\nAn organism is a complete UI app with event sourcing:\n- main.code: Initial version (HTML/CSS/JS/activities in YAML format)\n- Mutations: Incremental updates (JavaScript only)\n- Snapshots: Automatic every 20 mutations for fast loading\n\nNaming convention: a-zA-Z0-9_ only, max 4 words, descriptive of the app.\nExamples: customer_form, sales_dashboard, task_manager, chat_interface\n\nThe organism is saved to .plasma/organisms/{name}/ and can be:\n- Mutated incrementally with plasma_mutate\n- Loaded instantly with plasma_load\n- Optimized with plasma_optimize (LLM-based compaction)\n\nThis enables fast app loading and complete change history.",{name:n.string().describe("Organism name (a-zA-Z0-9_ only, max 4 words, descriptive)"),description:n.string().describe("Description of what this organism does"),html:n.string().describe("HTML content"),css:n.string().optional().describe("CSS styles"),js:n.string().optional().describe("JavaScript code"),activities:p(n.array(n.object({id:n.string(),type:n.enum(["button","input","canvas","custom"]),context:n.any().optional(),dataProvider:n.string().optional().describe("JS expression evaluated at event time. Result is included as 'provided' in the action payload.")}))).describe("Interactive elements"),state:n.any().optional().describe("Initial state data"),tags:p(n.array(n.string())).optional().describe("Tags for categorization")},async t=>{try{!function(t){if(!/^[a-zA-Z0-9_]+$/.test(t))throw new Error("Organism name must contain only a-zA-Z0-9_ characters (BASIC convention)");if(t.split("_").length>4)throw new Error("Organism name must be max 4 words separated by underscores")}(t.name);const n=a(g,t.name);if(i(n))throw new Error(`Organism '${t.name}' already exists. Use plasma_mutate to update it.`);c(n,{recursive:!0});const s={name:t.name,description:t.description,created:(new Date).toISOString(),updated:(new Date).toISOString(),mutations:0,tags:t.tags||[]},o=`---\nname: ${s.name}\ndescription: ${s.description}\ncreated: ${s.created}\nupdated: ${s.updated}\nmutations: ${s.mutations}\n${s.tags&&s.tags.length>0?`tags: [${s.tags.join(", ")}]`:""}\n---\n`;r(a(n,"manifest.yaml"),o,"utf-8");const m={html:t.html,css:t.css,js:t.js,activities:t.activities};return r(a(n,"main.code"),`---\nactivities:\n${(e=m).activities.map(t=>{let e=` - id: ${t.id}\n type: ${t.type}`;return t.context&&(e+=`\n context: ${JSON.stringify(t.context)}`),t.dataProvider&&(e+=`\n dataProvider: ${t.dataProvider}`),e}).join("\n")}\n---\nhtml: |\n ${e.html.split("\n").join("\n ")}\n\n${e.css?`css: |\n ${e.css.split("\n").join("\n ")}\n`:""}\n${e.js?`js: |\n ${e.js.split("\n").join("\n ")}`:""}\n`.trim(),"utf-8"),l.info(`Created organism ${t.name}`),{content:[{type:"text",text:`✅ Organism '${t.name}' created successfully\n\nSaved to: .plasma/organisms/${t.name}/\n\nFiles:\n- manifest.yaml (metadata)\n- main.code (${t.html.length+(t.css?.length||0)+(t.js?.length||0)} bytes)\n\nActivities: ${t.activities.length}\n${t.activities.map(t=>`- #${t.id} (${t.type})`).join("\n")}\n\nUse plasma_load("${t.name}", node_id) to render it on a node.\nUse plasma_mutate("${t.name}", js) to apply incremental updates.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_create failed: ${e}`),{content:[{type:"text",text:`❌ Failed to create organism: ${e}`}],isError:!0}}var e}),e("plasma_mutate","Apply an incremental mutation to an existing organism.\n\nMutations are JavaScript-only updates that modify the existing app.\nEach mutation is saved as N_[descriptive_name].code where N is a progressive number.\n\nExamples:\n- 1_add_validation.code\n- 2_change_theme_blue.code\n- 3_fix_button_layout.code\n\nMutations are applied in sequence when loading the organism.\nSnapshots are created automatically every 20 mutations for performance.\n\nThe mutation is immediately applied to any active rendering via dynamic_ui_update.",{name:n.string().describe("Organism name"),mutation_name:n.string().regex(/^[a-zA-Z0-9_]+$/).describe("Descriptive name for this mutation (a-zA-Z0-9_ only)"),js:n.string().describe("JavaScript code for the mutation (modifies existing state)"),node_id:n.string().optional().describe("Optional: node ID to apply mutation immediately")},async t=>{try{const e=a(g,t.name);if(!i(e))throw new Error(`Organism '${t.name}' not found. Use plasma_list to see available organisms.`);const n=a(e,"manifest.yaml"),s=o(n,"utf-8"),c=s.match(/mutations:\s*(\d+)/),m=(c?parseInt(c[1],10):0)+1,p=`${m}_${t.mutation_name}.code`,u=a(e,p);r(u,t.js,"utf-8");const f=s.replace(/updated:.*/,`updated: ${(new Date).toISOString()}`).replace(/mutations:\s*\d+/,`mutations: ${m}`);r(n,f,"utf-8");let y=!1;m%20==0&&(!function(t,e){const n=a(t,"main.code");if(!i(n))throw new Error("main.code not found");const s=h(t).filter(t=>t.num<=e);let c=`// Snapshot at mutation ${e}\n// Auto-generated: ${(new Date).toISOString()}\n\n`;c+=`// === main.code ===\n${o(n,"utf-8")}\n\n`;for(const t of s)c+=`// === ${t.num}_${t.name}.code ===\n`,c+=o(t.path,"utf-8")+"\n\n";const m=a(t,`snapshot_${e}.code`);r(m,c,"utf-8"),l.info(`Created snapshot at mutation ${e} for ${t}`)}(e,m),y=!0),l.info(`Applied mutation ${m} to organism ${t.name}`);let $=!1;if(t.node_id){const e=d.nodeRegistry.getNode(t.node_id);if(e&&e.capabilities?.includes("plasma")){const n={type:"dynamic_ui_update",js:t.js};d.channel&&(n.channel=d.channel),d.chatId&&(n.chatId=d.chatId),e.ws.send(JSON.stringify(n)),$=!0,l.info(`Applied mutation to node ${t.node_id}`)}}return{content:[{type:"text",text:`✅ Mutation applied to organism '${t.name}'\n\nMutation: ${m}_${t.mutation_name}.code\nTotal mutations: ${m}\n${y?`\n📸 Snapshot created (snapshot_${m}.code) for fast loading`:""}\n${$?`\n✅ Mutation applied to node ${t.node_id}`:""}\n\nThe mutation has been saved and will be applied automatically when loading the organism.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_mutate failed: ${e}`),{content:[{type:"text",text:`❌ Failed to apply mutation: ${e}`}],isError:!0}}}),e("plasma_load","Load a saved PLASMA organism and render it on an ElectroNode.\n\nThis loads the organism from disk with smart snapshot support:\n1. Finds best snapshot (if available) for fast loading\n2. Loads main.code if no snapshot\n3. Applies remaining mutations in sequence\n4. Renders via dynamic_ui_render\n\nUse plasma_list to see available organisms.",{name:n.string().describe("Organism name to load"),node_id:n.string().describe("Target node ID (get from dynamic_ui_list_nodes)")},async t=>{try{const e=a(g,t.name);if(!i(e))throw new Error(`Organism '${t.name}' not found. Use plasma_list to see available organisms.`);const n=a(e,"manifest.yaml"),r=o(n,"utf-8"),c=r.match(/name:\s*(.+)/),m=(r.match(/description:\s*(.+)/),r.match(/mutations:\s*(\d+)/)),p=c?c[1].trim():t.name,f=m?parseInt(m[1],10):0,y=h(e),$=function(t,e){if(!i(t))return null;const n=s(t),o=[];for(const t of n){const e=t.match(/^snapshot_(\d+)\.code$/);e&&o.push(parseInt(e[1],10))}if(0===o.length)return null;const r=e.length>0?e[e.length-1].num:0,c=o.filter(t=>t<=r);if(0===c.length)return null;const m=Math.max(...c);return a(t,`snapshot_${m}.code`)}(e,y);let _,v=0;if($&&i($)){const e=o($,"utf-8"),n=$.match(/snapshot_(\d+)\.code$/);v=n?parseInt(n[1],10):0;const a=e.match(/=== main\.code ===\n([\s\S]+?)(?:\n\/\/ ===|$)/);if(!a)throw new Error("Failed to parse snapshot");_=u(a[1]),l.info(`Loading organism ${t.name} from snapshot_${v}.code`)}else{const n=a(e,"main.code");_=u(o(n,"utf-8")),v=0,l.info(`Loading organism ${t.name} from main.code`)}const x=a(e,"state.json");let S=null;i(x)&&(S=JSON.parse(o(x,"utf-8")));let w=_.js||"";S&&(w=`window.__initialState = ${JSON.stringify(S)};\n\n${w}`);const b=d.nodeRegistry.getNode(t.node_id);if(!b)throw new Error(`Node ${t.node_id} not found or disconnected`);if(!b.capabilities?.includes("plasma"))throw new Error(`Node ${t.node_id} does not support Dynamic UI`);const I={type:"dynamic_ui",html:_.html,css:_.css||"",js:w,activities:_.activities};d.channel&&(I.channel=d.channel),d.chatId&&(I.chatId=d.chatId),b.ws.send(JSON.stringify(I));const E=y.filter(t=>t.num>v);return E.length>0&&setTimeout(()=>{for(const t of E){const e={type:"dynamic_ui_update",js:o(t.path,"utf-8")};d.channel&&(e.channel=d.channel),d.chatId&&(e.chatId=d.chatId),b.ws.send(JSON.stringify(e))}l.info(`Applied ${E.length} mutations to organism ${t.name}`)},100),l.info(`Loaded and rendered organism ${t.name} on node ${t.node_id}`),{content:[{type:"text",text:`✅ Organism '${p}' loaded and rendered\n\nNode: ${b.displayName||t.node_id}\nTotal mutations: ${f}\n${$?`Loaded from: snapshot_${v}.code`:"Loaded from: main.code"}\n${E.length>0?`Applied ${E.length} additional mutations`:""}\nActivities: ${_.activities.length}\n\nThe organism is now active on the node.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_load failed: ${e}`),{content:[{type:"text",text:`❌ Failed to load organism: ${e}`}],isError:!0}}}),e("plasma_list","List all saved PLASMA organisms with their metadata.",{},async()=>{if(!i(g))return{content:[{type:"text",text:"No organisms saved yet. Use plasma_create to create your first organism."}]};const t=s(g,{withFileTypes:!0}).filter(t=>t.isDirectory()&&!t.name.startsWith(".")).map(t=>t.name);if(0===t.length)return{content:[{type:"text",text:"No organisms saved yet. Use plasma_create to create your first organism."}]};const e=t.map(t=>{try{const e=a(g,t,"manifest.yaml");if(i(e)){const n=o(e,"utf-8"),a=n.match(/name:\s*(.+)/),i=n.match(/description:\s*(.+)/),s=n.match(/mutations:\s*(\d+)/),r=n.match(/tags:\s*\[(.+)\]/),c=a?a[1].trim():t,m=i?i[1].trim():"No description",d=s?s[1]:"0";return`- ${c} (${t}) — ${d} mutations${r?` [${r[1]}]`:""}\n ${m}`}return`- ${t} (no manifest)`}catch(e){return`- ${t} (error reading manifest)`}}).join("\n\n");return{content:[{type:"text",text:`${t.length} PLASMA organism(s) available:\n\n${e}`}]}}),e("plasma_delete","Delete a PLASMA organism permanently.",{name:n.string().describe("Organism name to delete")},async t=>{try{const e=a(g,t.name);if(!i(e))throw new Error(`Organism '${t.name}' not found`);return m(e,{recursive:!0,force:!0}),l.info(`Deleted organism ${t.name}`),{content:[{type:"text",text:`✅ Organism '${t.name}' deleted permanently`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_delete failed: ${e}`),{content:[{type:"text",text:`❌ Failed to delete organism: ${e}`}],isError:!0}}}),e("plasma_screenshot","Capture a screenshot of the DynamicUI (PLASMA) surface on a node.\n\nReturns the screenshot as a PNG image that you can see directly.\nUse this to verify visual rendering, debug layout issues, or confirm styling changes.",{node_id:n.string().describe("Target node ID")},async t=>{try{const e=await d.nodeRegistry.executeCommand(t.node_id,"dynamic_ui.screenshot",{},15e3);if(!e.ok)return{content:[{type:"text",text:`Screenshot failed: ${e.error??"Unknown error"}`}],isError:!0};const n=e.result?.image;if(!n)return{content:[{type:"text",text:"Screenshot failed: No image data returned"}],isError:!0};const s=a(d.plasmaRootDir,"screenshots");i(s)||c(s,{recursive:!0});const o=`screenshot_${Date.now()}.png`,m=a(s,o);return r(m,Buffer.from(n,"base64")),l.info(`Screenshot saved to ${m}`),{content:[{type:"image",data:n,mimeType:"image/png"},{type:"text",text:`Screenshot saved: ${m}`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`plasma_screenshot failed: ${e}`),{content:[{type:"text",text:`Screenshot failed: ${e}`}],isError:!0}}}),e("dynamic_ui_query","Execute JavaScript in the DynamicUI surface on a node and return the result.\n\nUse this to read runtime state from the UI — form values, computed data, DOM state, etc.\nThe JS runs in the same context as the rendered PLASMA organism (same iframe/webview).\n\nExamples:\n- Read a form field: \"document.getElementById('name').value\"\n- Read multiple fields: \"({name: document.getElementById('name').value, email: document.getElementById('email').value})\"\n- Read app state: \"JSON.stringify(window.app.records)\"\n- Check element visibility: \"document.getElementById('panel').style.display !== 'none'\"",{node_id:n.string().describe("Target node ID"),js:n.string().describe("JavaScript expression to evaluate. The result is returned to the agent.")},async t=>{try{const e=await d.nodeRegistry.executeCommand(t.node_id,"dynamic_ui.query",{js:t.js},1e4);if(!e.ok)return{content:[{type:"text",text:`❌ dynamic_ui_query failed: ${e.error??"Unknown error"}`}],isError:!0};return{content:[{type:"text",text:`Result: ${"string"==typeof e.result?e.result:JSON.stringify(e.result)}`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return l.error(`dynamic_ui_query failed: ${e}`),{content:[{type:"text",text:`❌ dynamic_ui_query failed: ${e}`}],isError:!0}}})]})}
@@ -1 +1 @@
1
- import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{createLogger as r}from"../utils/logger.js";const n=r("ServerTools");export function createServerToolsServer(r,o){const i=o||Intl.DateTimeFormat().resolvedOptions().timeZone;return e({name:"server-tools",version:"1.0.0",tools:[t("restart_server","Restart the GrabMeABeer server. This reloads config from disk and reinitializes all channels, agents, and services. Active conversations will be interrupted. Use this when you need to apply configuration changes or recover from issues.",{},async()=>{try{return n.info("Server restart triggered by agent"),await r(),{content:[{type:"text",text:"Server restarted successfully. All channels and services have been reinitialized with fresh configuration."}]}}catch(e){const t=e instanceof Error?e.message:String(e);return n.error(`Agent-triggered restart failed: ${t}`),{content:[{type:"text",text:`Server restart failed: ${t}`}],isError:!0}}}),t("get_current_time","Get the current date and time in the server's configured timezone. Use this tool whenever you need to know the current time, date, day of the week, or any time-related information. Do not guess or estimate the current time — always call this tool.",{},async()=>{const e=new Date,t=e.toLocaleString("en-GB",{timeZone:i,weekday:"long",year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),r=e.toISOString();return{content:[{type:"text",text:`Current time (${i}): ${t}\nISO 8601: ${r}\nUnix timestamp: ${Math.floor(e.getTime()/1e3)}`}]}})]})}
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{createLogger as r}from"../utils/logger.js";const n=r("ServerTools");export function createServerToolsServer(r,o){const i=o||Intl.DateTimeFormat().resolvedOptions().timeZone;return e({name:"server-tools",version:"1.0.0",tools:[t("restart_server","Restart the GrabMeABeer server. This reloads config from disk and reinitializes all channels, agents, and services. Active conversations will be interrupted. Use this when you need to apply configuration changes or recover from issues.",{},async()=>(n.info("Server restart triggered by agent"),setTimeout(()=>{r().catch(e=>{const t=e instanceof Error?e.message:String(e);n.error(`Agent-triggered restart failed: ${t}`)})},100),{content:[{type:"text",text:"Server restart initiated. The server will reconfigure momentarily."}]})),t("get_current_time","Get the current date and time in the server's configured timezone. Use this tool whenever you need to know the current time, date, day of the week, or any time-related information. Do not guess or estimate the current time — always call this tool.",{},async()=>{const e=new Date,t=e.toLocaleString("en-GB",{timeZone:i,weekday:"long",year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),r=e.toISOString();return{content:[{type:"text",text:`Current time (${i}): ${t}\nISO 8601: ${r}\nUnix timestamp: ${Math.floor(e.getTime()/1e3)}`}]}})]})}
@@ -61,6 +61,8 @@ You have 2 base memory tools + 5 concept graph tools:
61
61
 
62
62
  These tools should be used **automatically and proactively**. If you detect a gap, search. If you need context, search.
63
63
 
64
+ **Anti-underuse rule for the concept graph:** Every time you do a `memory_search` on a person, project, or fact, also launch `concept_query` or `concept_search` **in parallel**. Near-zero marginal cost, potentially different information. Text gives you raw details, the graph gives you structured relationships. Using both together is always better than using one alone. If you do memory_search without a concept query, you're throwing away half your memory.
65
+
64
66
  ### Concept Graph (SQLite)
65
67
 
66
68
  The concept graph lives in **`concepts.db`** (SQLite, in the dataDir). Navigable via MCP tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.18",
3
+ "version": "1.6.23",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",
@@ -68,7 +68,7 @@
68
68
  "@grammyjs/runner": "^2.0.3",
69
69
  "@hera-al/browser-server": "^1.0.5",
70
70
  "@hono/node-server": "^1.13.8",
71
- "@mariozechner/pi-ai": "^0.52.12",
71
+ "@mariozechner/pi-ai": "^0.56.1",
72
72
  "@types/markdown-it": "^14.1.2",
73
73
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
74
74
  "better-sqlite3": "^11.7.0",
@@ -1,116 +0,0 @@
1
- ---
2
- name: gog
3
- description: Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.
4
- homepage: https://gogcli.sh
5
- metadata:
6
- {
7
- "openclaw":
8
- {
9
- "emoji": "🎮",
10
- "requires": { "bins": ["gog"] },
11
- "install":
12
- [
13
- {
14
- "id": "brew",
15
- "kind": "brew",
16
- "formula": "steipete/tap/gogcli",
17
- "bins": ["gog"],
18
- "label": "Install gog (brew)",
19
- },
20
- ],
21
- },
22
- }
23
- ---
24
-
25
- # gog
26
-
27
- Use `gog` for Gmail/Calendar/Drive/Contacts/Sheets/Docs. Requires OAuth setup.
28
-
29
- Setup (once)
30
-
31
- - `gog auth credentials /path/to/client_secret.json`
32
- - `gog auth add you@gmail.com --services gmail,calendar,drive,contacts,docs,sheets`
33
- - `gog auth list`
34
-
35
- Common commands
36
-
37
- - Gmail search: `gog gmail search 'newer_than:7d' --max 10`
38
- - Gmail messages search (per email, ignores threading): `gog gmail messages search "in:inbox from:ryanair.com" --max 20 --account you@example.com`
39
- - Gmail send (plain): `gog gmail send --to a@b.com --subject "Hi" --body "Hello"`
40
- - Gmail send (multi-line): `gog gmail send --to a@b.com --subject "Hi" --body-file ./message.txt`
41
- - Gmail send (stdin): `gog gmail send --to a@b.com --subject "Hi" --body-file -`
42
- - Gmail send (HTML): `gog gmail send --to a@b.com --subject "Hi" --body-html "<p>Hello</p>"`
43
- - Gmail draft: `gog gmail drafts create --to a@b.com --subject "Hi" --body-file ./message.txt`
44
- - Gmail send draft: `gog gmail drafts send <draftId>`
45
- - Gmail reply: `gog gmail send --to a@b.com --subject "Re: Hi" --body "Reply" --reply-to-message-id <msgId>`
46
- - Calendar list events: `gog calendar events <calendarId> --from <iso> --to <iso>`
47
- - Calendar create event: `gog calendar create <calendarId> --summary "Title" --from <iso> --to <iso>`
48
- - Calendar create with color: `gog calendar create <calendarId> --summary "Title" --from <iso> --to <iso> --event-color 7`
49
- - Calendar update event: `gog calendar update <calendarId> <eventId> --summary "New Title" --event-color 4`
50
- - Calendar show colors: `gog calendar colors`
51
- - Drive search: `gog drive search "query" --max 10`
52
- - Contacts: `gog contacts list --max 20`
53
- - Sheets get: `gog sheets get <sheetId> "Tab!A1:D10" --json`
54
- - Sheets update: `gog sheets update <sheetId> "Tab!A1:B2" --values-json '[["A","B"],["1","2"]]' --input USER_ENTERED`
55
- - Sheets append: `gog sheets append <sheetId> "Tab!A:C" --values-json '[["x","y","z"]]' --insert INSERT_ROWS`
56
- - Sheets clear: `gog sheets clear <sheetId> "Tab!A2:Z"`
57
- - Sheets metadata: `gog sheets metadata <sheetId> --json`
58
- - Docs export: `gog docs export <docId> --format txt --out /tmp/doc.txt`
59
- - Docs cat: `gog docs cat <docId>`
60
-
61
- Calendar Colors
62
-
63
- - Use `gog calendar colors` to see all available event colors (IDs 1-11)
64
- - Add colors to events with `--event-color <id>` flag
65
- - Event color IDs (from `gog calendar colors` output):
66
- - 1: #a4bdfc
67
- - 2: #7ae7bf
68
- - 3: #dbadff
69
- - 4: #ff887c
70
- - 5: #fbd75b
71
- - 6: #ffb878
72
- - 7: #46d6db
73
- - 8: #e1e1e1
74
- - 9: #5484ed
75
- - 10: #51b749
76
- - 11: #dc2127
77
-
78
- Email Formatting
79
-
80
- - Prefer plain text. Use `--body-file` for multi-paragraph messages (or `--body-file -` for stdin).
81
- - Same `--body-file` pattern works for drafts and replies.
82
- - `--body` does not unescape `\n`. If you need inline newlines, use a heredoc or `$'Line 1\n\nLine 2'`.
83
- - Use `--body-html` only when you need rich formatting.
84
- - HTML tags: `<p>` for paragraphs, `<br>` for line breaks, `<strong>` for bold, `<em>` for italic, `<a href="url">` for links, `<ul>`/`<li>` for lists.
85
- - Example (plain text via stdin):
86
-
87
- ```bash
88
- gog gmail send --to recipient@example.com \
89
- --subject "Meeting Follow-up" \
90
- --body-file - <<'EOF'
91
- Hi Name,
92
-
93
- Thanks for meeting today. Next steps:
94
- - Item one
95
- - Item two
96
-
97
- Best regards,
98
- Your Name
99
- EOF
100
- ```
101
-
102
- - Example (HTML list):
103
- ```bash
104
- gog gmail send --to recipient@example.com \
105
- --subject "Meeting Follow-up" \
106
- --body-html "<p>Hi Name,</p><p>Thanks for meeting today. Here are the next steps:</p><ul><li>Item one</li><li>Item two</li></ul><p>Best regards,<br>Your Name</p>"
107
- ```
108
-
109
- Notes
110
-
111
- - Set `GOG_ACCOUNT=you@gmail.com` to avoid repeating `--account`.
112
- - For scripting, prefer `--json` plus `--no-input`.
113
- - Sheets values can be passed via `--values-json` (recommended) or as inline rows.
114
- - Docs supports export/cat/copy. In-place edits require a Docs API client (not in gog).
115
- - Confirm before sending mail or creating events.
116
- - `gog gmail search` returns one row per thread; use `gog gmail messages search` when you need every individual email returned separately.