@hera-al/server 1.6.18 → 1.6.21

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 +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{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}}})]})}
@@ -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.21",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",
@@ -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.