@hera-al/server 1.6.2 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/agent/agent-service.d.ts +2 -1
  2. package/dist/agent/agent-service.js +1 -1
  3. package/dist/agent/prompt-builder.js +1 -1
  4. package/dist/agent/session-agent.d.ts +27 -1
  5. package/dist/agent/session-agent.js +1 -1
  6. package/dist/commands/model.d.ts +21 -2
  7. package/dist/commands/model.js +1 -1
  8. package/dist/commands/models.d.ts +4 -2
  9. package/dist/commands/models.js +1 -1
  10. package/dist/commands/status.d.ts +2 -0
  11. package/dist/commands/status.js +1 -1
  12. package/dist/config.d.ts +61 -4
  13. package/dist/config.js +1 -1
  14. package/dist/gateway/channels/telegram.js +1 -1
  15. package/dist/nostromo/nostromo.js +1 -1
  16. package/dist/nostromo/ui-html-layout.js +1 -1
  17. package/dist/nostromo/ui-js-agent.js +1 -1
  18. package/dist/nostromo/ui-js-config.js +1 -1
  19. package/dist/nostromo/ui-js-core.js +1 -1
  20. package/dist/nostromo/ui-styles.js +1 -1
  21. package/dist/pi-agent-provider/index.d.ts +115 -0
  22. package/dist/pi-agent-provider/index.js +1 -0
  23. package/dist/pi-agent-provider/integration-example.d.ts +83 -0
  24. package/dist/pi-agent-provider/integration-example.js +1 -0
  25. package/dist/pi-agent-provider/pi-context-compactor.d.ts +116 -0
  26. package/dist/pi-agent-provider/pi-context-compactor.js +1 -0
  27. package/dist/pi-agent-provider/pi-mcp-bridge.d.ts +38 -0
  28. package/dist/pi-agent-provider/pi-mcp-bridge.js +1 -0
  29. package/dist/pi-agent-provider/pi-message-adapter.d.ts +93 -0
  30. package/dist/pi-agent-provider/pi-message-adapter.js +1 -0
  31. package/dist/pi-agent-provider/pi-query.d.ts +49 -0
  32. package/dist/pi-agent-provider/pi-query.js +1 -0
  33. package/dist/pi-agent-provider/pi-skill-loader.d.ts +15 -0
  34. package/dist/pi-agent-provider/pi-skill-loader.js +1 -0
  35. package/dist/pi-agent-provider/pi-tool-adapter.d.ts +105 -0
  36. package/dist/pi-agent-provider/pi-tool-adapter.js +1 -0
  37. package/dist/pi-agent-provider/pi-tool-executor.d.ts +95 -0
  38. package/dist/pi-agent-provider/pi-tool-executor.js +1 -0
  39. package/dist/pi-agent-provider/pi-types.d.ts +179 -0
  40. package/dist/pi-agent-provider/pi-types.js +1 -0
  41. package/dist/server.js +1 -1
  42. package/dist/stt/stt-loader.js +1 -1
  43. package/dist/tools/pico-tools.d.ts +11 -0
  44. package/dist/tools/pico-tools.js +1 -0
  45. package/package.json +2 -1
@@ -18,9 +18,10 @@ export declare class AgentService {
18
18
  private ttsToolsServer;
19
19
  private memoryToolsServer;
20
20
  private browserToolsServer;
21
+ private picoToolsServer;
21
22
  private channelManager;
22
23
  private showToolUseGetter;
23
- constructor(config: AppConfig, nodeRegistry?: NodeRegistry, channelManager?: ChannelManager, serverToolsServer?: ReturnType<typeof createServerToolsServer>, cronToolsServer?: unknown, sessionDb?: import("./session-db.js").SessionDB, ttsToolsServer?: ReturnType<typeof createTTSToolsServer>, memoryToolsServer?: unknown, showToolUseGetter?: (sessionKey: string) => boolean, browserToolsServer?: unknown);
24
+ constructor(config: AppConfig, nodeRegistry?: NodeRegistry, channelManager?: ChannelManager, serverToolsServer?: ReturnType<typeof createServerToolsServer>, cronToolsServer?: unknown, sessionDb?: import("./session-db.js").SessionDB, ttsToolsServer?: ReturnType<typeof createTTSToolsServer>, memoryToolsServer?: unknown, showToolUseGetter?: (sessionKey: string) => boolean, browserToolsServer?: unknown, picoToolsServer?: unknown);
24
25
  /**
25
26
  * Send a message to the agent for a given session.
26
27
  * Creates a long-lived SessionAgent on first call; subsequent messages for
@@ -1 +1 @@
1
- import{query as e}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as s}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as o}from"../tools/message-tools.js";import{createLogger as r}from"../utils/logger.js";const n=r("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsServer=null;messageToolsServer=null;serverToolsServer=null;cronToolsServer=null;ttsToolsServer=null;memoryToolsServer=null;browserToolsServer=null;channelManager=null;showToolUseGetter=null;constructor(e,s,r,n,i,a,l,g,h,c){this.config=e,r&&(this.channelManager=r),s&&(this.nodeToolsServer=t(s)),r&&a&&(this.messageToolsServer=o(r,()=>this.config,a)),n&&(this.serverToolsServer=n),i&&(this.cronToolsServer=i),l&&(this.ttsToolsServer=l),g&&(this.memoryToolsServer=g),c&&(this.browserToolsServer=c),h&&(this.showToolUseGetter=h)}async sendMessage(e,s,t,o,r,i,a,l,g,h){const c=this.getOrCreateAgent(e,t,o,r,i,a,l,g,h);if(i&&i!==c.getModel()){const t=e=>{const s=this.config.models.find(s=>s.id===e);return!(!s?.proxy||"not-used"===s.proxy)},d=t(c.getModel()),u=t(i);if(d||u){n.info(`[${e}] Proxy config change detected (old=${d}, new=${u}), restarting session`),this.destroySession(e);const t=this.getOrCreateAgent(e,void 0,o,r,i,a,l,g,h);return await t.send(s)}await c.setModel(i)}try{return await c.send(s)}catch(d){const u=d instanceof Error?d.message:String(d);if(u.includes("INVALID_ARGUMENT")||/API Error: 400/.test(u)){n.warn(`[${e}] Transient API 400 error, retrying once: ${u.slice(0,120)}`),this.agents.delete(e),c.close();const d=this.getOrCreateAgent(e,t,o,r,i,a,l,g,h);try{return await d.send(s)}catch(s){n.error(`[${e}] Retry also failed: ${s}`),this.agents.delete(e),d.close();const o=s instanceof Error?s.message:String(s);if(t)return{response:o.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw s}}if(this.agents.delete(e),c.close(),t)return n.warn(`Session agent failed for ${e}: ${d}`),{response:u.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw d}}hasNodeTools(){return null!==this.nodeToolsServer}hasMessageTools(){return null!==this.messageToolsServer}getToolServers(){const e=[];return this.nodeToolsServer&&e.push(this.nodeToolsServer),this.messageToolsServer&&e.push(this.messageToolsServer),this.serverToolsServer&&e.push(this.serverToolsServer),this.cronToolsServer&&e.push(this.cronToolsServer),this.ttsToolsServer&&e.push(this.ttsToolsServer),this.memoryToolsServer&&e.push(this.memoryToolsServer),this.browserToolsServer&&e.push(this.browserToolsServer),e}getOrCreateAgent(e,t,o,r,n,i,a,l,g){const h=this.agents.get(e);if(h&&h.isActive())return h;h&&(h.close(),this.agents.delete(e));const c=new s(e,this.config,o,r,t,n,this.nodeToolsServer??void 0,this.messageToolsServer??void 0,this.serverToolsServer??void 0,this.cronToolsServer??void 0,i,a,l,this.ttsToolsServer??void 0,this.memoryToolsServer??void 0,this.browserToolsServer??void 0,g);if(this.channelManager){const e=this.channelManager;if(c.setChannelSender(async(s,t,o,r)=>{r&&r.length>0?await e.sendButtons(s,t,o,r):await e.sendToChannel(s,t,o)}),this.showToolUseGetter){const s=this.showToolUseGetter;c.setToolUseNotifier(async(t,o,r)=>{if(!s(`${t}:${o}`))return;const n=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await e.sendToChannel(t,o,n),await e.setTyping(t,o)})}c.setTypingSetter(async(s,t)=>{await e.setTyping(s,t)}),c.setTypingClearer(async(s,t)=>{await e.clearTyping(s,t)}),c.setTextBlockStreamer(async(s,t,o)=>{await e.sendResponse(s,t,o)})}return c.setUsageRecorder((e,s,t,o,r)=>{this.usageBySession.set(e,{totalCostUsd:s,durationMs:t,numTurns:o,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(e,c),c}async interrupt(e){const s=this.agents.get(e);return!!s&&s.interrupt()}isBusy(e){const s=this.agents.get(e);return!!s&&s.isBusy()}hasPendingPermission(e){const s=this.agents.get(e);return!!s&&s.hasPendingPermission()}resolvePermission(e,s){const t=this.agents.get(e);t&&t.resolvePermission(s)}hasPendingQuestion(e){const s=this.agents.get(e);return!!s&&s.hasPendingQuestion()}resolveQuestion(e,s){const t=this.agents.get(e);t&&t.resolveQuestion(s)}destroySession(e){const s=this.agents.get(e);s&&(s.close(),this.agents.delete(e),n.info(`Session agent destroyed: ${e}`))}destroyAll(){for(const[e,s]of this.agents)s.close(),n.info(`Session agent destroyed (reconfigure): ${e}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(e=>{const s=this.agents.get(e);return s&&s.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(e){return this.usageBySession.get(e)}getSdkSlashCommands(){for(const e of this.agents.values()){const s=e.getSdkSlashCommands();if(s.length>0)return s}return[]}async listModels(){try{const s=e({prompt:"list models",options:{maxTurns:0}}),t=await s.supportedModels();for await(const e of s)break;return t.map(e=>({id:e.id??e.name??String(e),name:e.name??e.id??String(e)}))}catch(e){return n.error(`Failed to list models: ${e}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
1
+ import{query as e}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as s}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as o}from"../tools/message-tools.js";import{createLogger as r}from"../utils/logger.js";const n=r("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsServer=null;messageToolsServer=null;serverToolsServer=null;cronToolsServer=null;ttsToolsServer=null;memoryToolsServer=null;browserToolsServer=null;picoToolsServer=null;channelManager=null;showToolUseGetter=null;constructor(e,s,r,n,i,a,l,h,g,c,d){this.config=e,r&&(this.channelManager=r),s&&(this.nodeToolsServer=t(s)),r&&a&&(this.messageToolsServer=o(r,()=>this.config,a)),n&&(this.serverToolsServer=n),i&&(this.cronToolsServer=i),l&&(this.ttsToolsServer=l),h&&(this.memoryToolsServer=h),c&&(this.browserToolsServer=c),d&&(this.picoToolsServer=d),g&&(this.showToolUseGetter=g)}async sendMessage(e,s,t,o,r,i,a,l,h,g){const c=this.getOrCreateAgent(e,t,o,r,i,a,l,h,g);if(i&&i!==c.getModel()){const t=e=>{const s=this.config.models.find(s=>s.id===e);return!(!s?.proxy||"not-used"===s.proxy)},d=t(c.getModel()),u=t(i);if(d||u){n.info(`[${e}] Proxy config change detected (old=${d}, new=${u}), restarting session`),this.destroySession(e);const t=this.getOrCreateAgent(e,void 0,o,r,i,a,l,h,g);return await t.send(s)}await c.setModel(i)}try{return await c.send(s)}catch(d){const u=d instanceof Error?d.message:String(d);if(u.includes("INVALID_ARGUMENT")||/API Error: 400/.test(u)){n.warn(`[${e}] Transient API 400 error, retrying once: ${u.slice(0,120)}`),this.agents.delete(e),c.close();const d=this.getOrCreateAgent(e,t,o,r,i,a,l,h,g);try{return await d.send(s)}catch(s){n.error(`[${e}] Retry also failed: ${s}`),this.agents.delete(e),d.close();const o=s instanceof Error?s.message:String(s);if(t)return{response:o.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw s}}if(this.agents.delete(e),c.close(),t)return n.warn(`Session agent failed for ${e}: ${d}`),{response:u.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw d}}hasNodeTools(){return null!==this.nodeToolsServer}hasMessageTools(){return null!==this.messageToolsServer}getToolServers(){const e=[];return this.nodeToolsServer&&e.push(this.nodeToolsServer),this.messageToolsServer&&e.push(this.messageToolsServer),this.serverToolsServer&&e.push(this.serverToolsServer),this.cronToolsServer&&e.push(this.cronToolsServer),this.ttsToolsServer&&e.push(this.ttsToolsServer),this.memoryToolsServer&&e.push(this.memoryToolsServer),this.browserToolsServer&&e.push(this.browserToolsServer),this.picoToolsServer&&e.push(this.picoToolsServer),e}getOrCreateAgent(e,t,o,r,n,i,a,l,h){const g=this.agents.get(e);if(g&&g.isActive())return g;g&&(g.close(),this.agents.delete(e));const c=new s(e,this.config,o,r,t,n,this.nodeToolsServer??void 0,this.messageToolsServer??void 0,this.serverToolsServer??void 0,this.cronToolsServer??void 0,i,a,l,this.ttsToolsServer??void 0,this.memoryToolsServer??void 0,this.browserToolsServer??void 0,h,this.picoToolsServer??void 0);if(this.channelManager){const e=this.channelManager;if(c.setChannelSender(async(s,t,o,r)=>{r&&r.length>0?await e.sendButtons(s,t,o,r):await e.sendToChannel(s,t,o)}),this.showToolUseGetter){const s=this.showToolUseGetter;c.setToolUseNotifier(async(t,o,r)=>{if(!s(`${t}:${o}`))return;const n=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await e.sendToChannel(t,o,n),await e.setTyping(t,o)})}c.setTypingSetter(async(s,t)=>{await e.setTyping(s,t)}),c.setTypingClearer(async(s,t)=>{await e.clearTyping(s,t)}),c.setTextBlockStreamer(async(s,t,o)=>{await e.sendResponse(s,t,o)})}return c.setUsageRecorder((e,s,t,o,r)=>{this.usageBySession.set(e,{totalCostUsd:s,durationMs:t,numTurns:o,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(e,c),c}async interrupt(e){const s=this.agents.get(e);return!!s&&s.interrupt()}isBusy(e){const s=this.agents.get(e);return!!s&&s.isBusy()}hasPendingPermission(e){const s=this.agents.get(e);return!!s&&s.hasPendingPermission()}resolvePermission(e,s){const t=this.agents.get(e);t&&t.resolvePermission(s)}hasPendingQuestion(e){const s=this.agents.get(e);return!!s&&s.hasPendingQuestion()}resolveQuestion(e,s){const t=this.agents.get(e);t&&t.resolveQuestion(s)}destroySession(e){const s=this.agents.get(e);s&&(s.close(),this.agents.delete(e),n.info(`Session agent destroyed: ${e}`))}destroyAll(){for(const[e,s]of this.agents)s.close(),n.info(`Session agent destroyed (reconfigure): ${e}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(e=>{const s=this.agents.get(e);return s&&s.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(e){return this.usageBySession.get(e)}getSdkSlashCommands(){for(const e of this.agents.values()){const s=e.getSdkSlashCommands();if(s.length>0)return s}return[]}async listModels(){try{const s=e({prompt:"list models",options:{maxTurns:0}}),t=await s.supportedModels();for await(const e of s)break;return t.map(e=>({id:e.id??e.name??String(e),name:e.name??e.id??String(e)}))}catch(e){return n.error(`Failed to list models: ${e}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
@@ -1 +1 @@
1
- import{hostname as e,type as n,release as t,arch as s}from"node:os";import{loadTemplate as o,loadBuiltInTools as a,formatWorkspaceFiles as r,filterForSubagent as i}from"./workspace-files.js";import{createLogger as l}from"../utils/logger.js";const c=l("PromptBuilder");export function buildSystemPrompt(l){const{config:h,sessionContext:u,mode:p,hasNodeTools:g,hasMessageTools:f,coderSkill:y,subagentTask:T,toolServers:_}=l,E="minimal"===p?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",I=o(h.dataDir,E),S="minimal"===p?i(l.workspaceFiles):l.workspaceFiles;var w;const b=resolvePlaceholders(I,{SESSION_KEY:u.sessionKey,CHANNEL:u.channel,CHAT_ID:u.chatId,SESSION_ID:u.sessionId||"(new session)",MODEL:h.agent.model,HOSTNAME:e(),OS:`${n()} ${t()} (${s()})`,WORKSPACE_DIR:h.agent.workspacePath,DATA_DIR:h.dataDir,MEMORY_FILE:u.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:u.attachmentsDir||"(memory disabled)",TIMEZONE:h.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,SUBAGENT_TASK:T??"",NODE_TOOLS_INSTRUCTIONS:g?"# 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:f?"# 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:h.cron.enabled?(w=h.cron.heartbeat.message,`# Heartbeats\n\nHeartbeat prompt: ${w}\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:h.cron.enabled?h.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:y?"":a(h.dataDir,p),SEARCH_IN_MEMORIES:"builtin-only"!==h.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:d(_),RUNTIME_LINE:m(h,u),WORKSPACE_FILES:r(S)}).replace(/\n{3,}/g,"\n\n");return c.debug(`System prompt built (mode=${p}, template=${E}, length=${b.length})`),b}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(c.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function d(e){if(!e||0===e.length)return"";const n=[];for(const t of e){const e=t,s=e.instance?._registeredTools;if(s)for(const[e,t]of Object.entries(s)){const s=t.description||"",o=t.inputSchema?.def?.shape,a=[];if(o)for(const[e,n]of Object.entries(o)){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 s=n.isOptional?.()??!1;a.push(`${e}${s?"?":""}: ${t}`)}const r=a.length>0?`Params: { ${a.join(", ")} }`:"No parameters.",i=s.match(/\.\s*(Returns?\s.+?)\.?\s*$/i),l=i?i[1].trim():"";let c=`- **${e}**: ${i?s.slice(0,i.index).trim():s.trim()}. ${r}`;l&&(c+=`. ${l}.`),n.push(c)}}return 0===n.length?"":n.join("\n")}function m(o,a){return`host=${e()} | os=${n()} ${t()} (${s()}) | model=${o.agent.model} | channel=${a.channel} | session=${a.sessionKey}`}export function buildPrompt(e,n,t){const s=[],o=[];n&&s.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?s.push(n.text):"image"===n.type&&n.imageBase64&&o.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});if(e.savedFiles.length>0){s.push(""),s.push("Files available in the current working directory:");for(const n of e.savedFiles)s.push(`- ${n}`)}return{text:s.join("\n"),images:o}}
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}}
@@ -71,6 +71,8 @@ export declare class SessionAgent {
71
71
  private model;
72
72
  private queueMode;
73
73
  private closed;
74
+ /** Pi provider config — non-null when engine.type === "pi". */
75
+ private piProviderConfig;
74
76
  private outputDone;
75
77
  private initialized;
76
78
  private opts;
@@ -91,11 +93,30 @@ export declare class SessionAgent {
91
93
  private textBlockStreamer;
92
94
  private pendingTextBlock;
93
95
  private streamedAny;
96
+ /** Accumulated text already flushed to the channel via textBlockStreamer. */
97
+ private streamedText;
94
98
  private usageRecorder;
95
99
  private autoApproveTools;
96
100
  private pendingPermission;
97
101
  private pendingQuestion;
98
- constructor(sessionKey: string, config: AppConfig, systemPrompt: string, subagentSystemPrompt: string, sessionId?: string, modelOverride?: string, nodeToolsServer?: unknown, messageToolsServer?: unknown, serverToolsServer?: unknown, cronToolsServer?: unknown, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, ttsToolsServer?: unknown, memoryToolsServer?: unknown, browserToolsServer?: unknown, sandboxEnabled?: boolean);
102
+ constructor(sessionKey: string, config: AppConfig, systemPrompt: string, subagentSystemPrompt: string, sessionId?: string, modelOverride?: string, nodeToolsServer?: unknown, messageToolsServer?: unknown, serverToolsServer?: unknown, cronToolsServer?: unknown, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, ttsToolsServer?: unknown, memoryToolsServer?: unknown, browserToolsServer?: unknown, sandboxEnabled?: boolean, picoToolsServer?: unknown);
103
+ /**
104
+ * Resolve Pi provider configuration based on the active model.
105
+ *
106
+ * Logic:
107
+ * 1. Determine active model name (session override or config.agent.model)
108
+ * 2. If picoAgent has a modelRef matching that name → use pi engine
109
+ * 3. Else fallback to legacy engine config (engine.type === "pi")
110
+ * 4. Otherwise → null (use SDK)
111
+ *
112
+ * piModelRef formats (colon-separated):
113
+ * New 3-part: "ModelName:piProvider:piModelId"
114
+ * Old 2-part: "ModelName:provider/modelId" (backward compat)
115
+ *
116
+ * Also extracts contextWindow and cost fields from the model entry
117
+ * for compaction thresholds and cost tracking.
118
+ */
119
+ private resolvePiConfig;
99
120
  /**
100
121
  * Send a prompt and wait for the agent's response.
101
122
  * Behavior depends on queueMode when the agent is already busy.
@@ -188,6 +209,11 @@ export declare class SessionAgent {
188
209
  */
189
210
  private mergePrompts;
190
211
  private ensureInitialized;
212
+ /**
213
+ * Initialize the Pi Agent engine as an alternative to the Claude Agent SDK.
214
+ * Dynamically imports the pi-agent-provider library and sets up piQuery().
215
+ */
216
+ private initPiEngine;
191
217
  private buildQueueMessage;
192
218
  /**
193
219
  * Background loop that reads SDK output and maps results to pending callers.
@@ -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 o}from"../utils/logger.js";const i=o("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;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;usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,o,n,r,l,a,u,h,c,p,d,g,m,f,y,$,b){this.sessionKey=e,this.config=o,this.currentSessionId=l??"";const T=a??o.agent.model;this.model=T?t(o,T):"",this.queueMode=o.agent.queueMode,this.debounceMs=Math.max(0,o.agent.queueDebounceMs),this.queueCap=Math.max(0,o.agent.queueCap),this.dropPolicy=o.agent.queueDropPolicy,this.autoApproveTools=o.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:d?{type:"preset",preset:"claude_code",append:n}:n,...o.agent.maxTurns>0?{maxTurns:o.agent.maxTurns}:{},cwd:o.agent.workspacePath,env:process.env,permissionMode:o.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(i.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),o=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,o,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{i.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const w=o.agent.settingSources;"user"===w?this.opts.settingSources=["user"]:"project"===w?this.opts.settingSources=["project"]:"both"===w&&(this.opts.settingSources=["user","project"]);const S=o.agent.mainFallback;S&&(this.opts.fallbackModel=t(o,S)),o.agent.allowedTools.length>0&&(this.opts.allowedTools=o.agent.allowedTools),o.agent.disallowedTools.length>0&&(this.opts.disallowedTools=o.agent.disallowedTools);const v={};if(Object.keys(o.agent.mcpServers).length>0&&Object.assign(v,o.agent.mcpServers),u&&(v["node-tools"]=u),h&&(v["message-tools"]=h),c&&(v["server-tools"]=c),p&&(v["cron-tools"]=p),f&&(v["tts-tools"]=f),y&&(v["memory-tools"]=y),$&&(v["browser-tools"]=$),Object.keys(v).length>0&&(this.opts.mcpServers=v,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(v)){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 o.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 K=o.agent.plugins.filter(e=>e.enabled);K.length>0&&(this.opts.options={...this.opts.options,plugins:K.map(e=>({type:"local",path:e.path}))});const _=this.buildEnvForModel(this.model);this.opts.env=_.env,_.disableThinking&&(this.opts.maxThinkingTokens=0)}async send(e){if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");switch(this.ensureInitialized(),this.queueMode){case"collect":return this.sendCollect(e);case"steer":return this.sendSteer(e);default:return this.sendDirect(e)}}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),i.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,i.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){i.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=[],i.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="",i.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,i.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?(i.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(i.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,i.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return i.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),o=this.sessionKey.substring(e+1);if(!t||!o||"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 u=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,o)}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,o,u,e)}else await this.channelSender(t,o,u)}catch(e){return i.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const h=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";i.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,o,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},h);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,o)}catch{}}return i.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return i.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return i.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 o=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!o||!n||"cron"===o)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(o,n,l,a)}catch(e){return i.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const u=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,i.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(o,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},u);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),o=this.sessionKey.substring(s+1);if(!t||!o||"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,o,l,e)}else await this.channelSender(t,o,l)}catch(e){i.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,o)}catch(e){i.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),o=this.sessionKey.substring(s+1);if(t&&o&&"cron"!==t)try{await this.toolUseNotifier(t,o,e)}catch(e){i.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 o=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0;try{await this.textBlockStreamer(s,t,o)}catch(e){i.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return i.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),i.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(),i.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&&(i.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return i.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}),i.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),o=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(o),i.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 o=0;o<e.length;o++)e[o].text&&s.push(`${o+1}. ${e[o].text}`),t.push(...e[o].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){this.initialized||(this.initialized=!0,i.info(`[${this.sessionKey}] Starting agent: model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}buildQueueMessage(e){if(0===e.images.length)return i.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}),i.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(i.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(" "),i.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...o}=s;this.currentResponse=JSON.stringify(o,null,2),i.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 o=s.map(e=>e.type).join(", ");i.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${o}], 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);i.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&this.notifyToolUse(e.name).catch(e=>{i.error(`[${this.sessionKey}] Tool use notification error: ${e}`)})}}if("tool_progress"===e.type){const s=e;i.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;i.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 o=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&(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,o]of Object.entries(s.modelUsage)){const s=o,i=[` ${t}:`];s.inputTokens&&i.push(`input=${s.inputTokens}`),s.outputTokens&&i.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&i.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&i.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&i.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(i.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}"refusal"===o?(i.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===o&&i.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",i.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",i.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?i.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):i.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||""),i.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:o})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,"collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){i.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();s&&(this.currentSessionId?(i.warn(`[${this.sessionKey}] Session corrupted: ${this.currentSessionId}`),s.resolve({response:"",sessionId:"",sessionReset:!0})):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 o}from"../utils/logger.js";const i=o("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,o,n,r,l,a,h,u,d,c,p,g,f,m,y,$,b,v){this.sessionKey=e,this.config=o,this.currentSessionId=l??"";const T=a??o.agent.model;this.model=T?t(o,T):"",this.queueMode=o.agent.queueMode,this.debounceMs=Math.max(0,o.agent.queueDebounceMs),this.queueCap=Math.max(0,o.agent.queueCap),this.dropPolicy=o.agent.queueDropPolicy,this.autoApproveTools=o.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...o.agent.maxTurns>0?{maxTurns:o.agent.maxTurns}:{},cwd:o.agent.workspacePath,env:process.env,permissionMode:o.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(i.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),o=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,o,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{i.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const w=o.agent.settingSources;"user"===w?this.opts.settingSources=["user"]:"project"===w?this.opts.settingSources=["project"]:"both"===w&&(this.opts.settingSources=["user","project"]);const S=o.agent.mainFallback;S&&(this.opts.fallbackModel=t(o,S)),o.agent.allowedTools.length>0&&(this.opts.allowedTools=o.agent.allowedTools),o.agent.disallowedTools.length>0&&(this.opts.disallowedTools=o.agent.disallowedTools);const x={};if(Object.keys(o.agent.mcpServers).length>0&&Object.assign(x,o.agent.mcpServers),h&&(x["node-tools"]=h),u&&(x["message-tools"]=u),d&&(x["server-tools"]=d),c&&(x["cron-tools"]=c),m&&(x["tts-tools"]=m),y&&(x["memory-tools"]=y),$&&(x["browser-tools"]=$),v&&(x["pico-tools"]=v),Object.keys(x).length>0&&(this.opts.mcpServers=x,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(x)){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"))),f){const e={};for(const s of o.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 R=o.agent.plugins.filter(e=>e.enabled);R.length>0&&(this.opts.options={...this.opts.options,plugins:R.map(e=>({type:"local",path:e.path}))});const K=this.buildEnvForModel(this.model);this.opts.env=K.env,K.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&i.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 o;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(o=n.modelRefs.find(e=>e.split(":")[0]===t)),!o){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;o=e.piModelRef}const r=o.split(":");if(r.length<2)return i.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${o}`),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);return u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(i.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),i.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`),{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}}async send(e){if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");switch(this.ensureInitialized(),this.queueMode){case"collect":return this.sendCollect(e);case"steer":return this.sendSteer(e);default:return this.sendDirect(e)}}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),i.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,i.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){i.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=[],i.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="",i.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,i.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?(i.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(i.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,i.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return i.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),o=this.sessionKey.substring(e+1);if(!t||!o||"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,o)}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,o,h,e)}else await this.channelSender(t,o,h)}catch(e){return i.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,d=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";i.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,o,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=d,this.typingSetter)try{await this.typingSetter(t,o)}catch{}}return i.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return i.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return i.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 o=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!o||!n||"cron"===o)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(o,n,l,a)}catch(e){return i.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(o,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,i.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(o,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),o=this.sessionKey.substring(s+1);if(!t||!o||"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,o,l,e)}else await this.channelSender(t,o,l)}catch(e){i.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,o)}catch(e){i.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),o=this.sessionKey.substring(s+1);if(t&&o&&"cron"!==t)try{await this.toolUseNotifier(t,o,e)}catch(e){i.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 o=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=o;try{await this.textBlockStreamer(s,t,o)}catch(e){i.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return i.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),i.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(),i.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&&(i.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return i.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}),i.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),o=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(o),i.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 o=0;o<e.length;o++)e[o].text&&s.push(`${o+1}. ${e[o].text}`),t.push(...e[o].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";i.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(),i.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){i.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),i.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 i.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}),i.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(i.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(" "),i.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...o}=s;this.currentResponse=JSON.stringify(o,null,2),i.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 o=s.map(e=>e.type).join(", ");i.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${o}], 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);i.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&this.notifyToolUse(e.name).catch(e=>{i.error(`[${this.sessionKey}] Tool use notification error: ${e}`)})}}if("tool_progress"===e.type){const s=e;i.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;i.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 o=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&(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,o]of Object.entries(s.modelUsage)){const s=o,i=[` ${t}:`];s.inputTokens&&i.push(`input=${s.inputTokens}`),s.outputTokens&&i.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&i.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&i.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&i.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(i.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse){const e=this.piProviderConfig;i.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${o}. Check provider routing and API key.`)}"refusal"===o?(i.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===o&&i.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",i.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",i.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?i.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):i.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)),i.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:o})}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){i.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();s&&(this.currentSessionId?(i.warn(`[${this.sessionKey}] Session corrupted: ${this.currentSessionId}`),s.resolve({response:"",sessionId:"",sessionReset:!0})):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}}}
@@ -9,14 +9,33 @@ export interface ModelEntry {
9
9
  apiKey?: string;
10
10
  baseURL?: string;
11
11
  }
12
+ export interface PicoAgentInfo {
13
+ enabled: boolean;
14
+ modelRefs: string[];
15
+ }
12
16
  export type ModelRegistryProvider = () => ModelEntry[];
13
- export type ModelSetHandler = (sessionKey: string, modelId: string) => Promise<void>;
17
+ export type PicoAgentProvider = () => PicoAgentInfo;
18
+ /** Returns true when the session was reset (engine type change). */
19
+ export type ModelSetHandler = (sessionKey: string, modelId: string) => Promise<boolean>;
20
+ export type DefaultModelSetHandler = (modelId: string) => Promise<void>;
21
+ /** Filter models eligible for agent use. Includes internal, proxied external, and pico-agent external models. */
22
+ export declare function filterEligibleModels(models: ModelEntry[], pico?: PicoAgentInfo): ModelEntry[];
14
23
  export declare class ModelCommand implements Command {
15
24
  private registryProvider;
16
25
  private setHandler;
26
+ private picoAgentProvider?;
27
+ name: string;
28
+ description: string;
29
+ constructor(registryProvider: ModelRegistryProvider, setHandler: ModelSetHandler, picoAgentProvider?: PicoAgentProvider | undefined);
30
+ execute(ctx: CommandContext): Promise<CommandResult>;
31
+ }
32
+ export declare class DefaultModelCommand implements Command {
33
+ private registryProvider;
34
+ private setHandler;
35
+ private picoAgentProvider?;
17
36
  name: string;
18
37
  description: string;
19
- constructor(registryProvider: ModelRegistryProvider, setHandler: ModelSetHandler);
38
+ constructor(registryProvider: ModelRegistryProvider, setHandler: DefaultModelSetHandler, picoAgentProvider?: PicoAgentProvider | undefined);
20
39
  execute(ctx: CommandContext): Promise<CommandResult>;
21
40
  }
22
41
  //# sourceMappingURL=model.d.ts.map
@@ -1 +1 @@
1
- export class ModelCommand{registryProvider;setHandler;name="model";description="Switch model for this session (usage: /model <name or id>)";constructor(e,t){this.registryProvider=e,this.setHandler=t}async execute(e){const t=e.args.trim();if(!t)return{text:"Usage: /model <name or id>\nUse /models to see available models."};const n=this.registryProvider().filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy}),s=t.toLowerCase();let i=n.find(e=>e.name===t)??n.find(e=>e.id===t)??n.find(e=>e.name.toLowerCase()===s)??n.find(e=>e.id.toLowerCase()===s);if(!i){const e=n.filter(e=>e.name.toLowerCase().includes(s)||e.id.toLowerCase().includes(s));if(1===e.length)i=e[0];else if(e.length>1){const n=e.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`"${t}" matches multiple models:\n${n}\n\nPlease be more specific.`}}}if(!i){const e=n.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`Model "${t}" not found.\n\nAvailable models:\n${e}`}}return await this.setHandler(e.sessionKey,i.id),{text:`Model switched to: ${i.name} (${i.id})`}}}
1
+ export function filterEligibleModels(e,t){const n=new Set;if(t?.enabled)for(const e of t.modelRefs){const t=e.split(":")[0]?.trim();t&&n.add(t)}return e.filter(e=>{const i=e.types||["external"];return i.includes("internal")||i.includes("external")&&e.proxy&&"not-used"!==e.proxy||t?.enabled&&i.includes("external")&&n.has(e.name)})}export class ModelCommand{registryProvider;setHandler;picoAgentProvider;name="model";description="Switch model for this session (usage: /model <name or id>)";constructor(e,t,n){this.registryProvider=e,this.setHandler=t,this.picoAgentProvider=n}async execute(e){const t=e.args.trim();if(!t)return{text:"Usage: /model <name or id>\nUse /models to see available models."};const n=this.picoAgentProvider?.(),i=filterEligibleModels(this.registryProvider(),n),s=t.toLowerCase();let o=i.find(e=>e.name===t)??i.find(e=>e.id===t)??i.find(e=>e.name.toLowerCase()===s)??i.find(e=>e.id.toLowerCase()===s);if(!o){const e=i.filter(e=>e.name.toLowerCase().includes(s)||e.id.toLowerCase().includes(s));if(1===e.length)o=e[0];else if(e.length>1){const n=e.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`"${t}" matches multiple models:\n${n}\n\nPlease be more specific.`}}}if(!o){const e=i.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`Model "${t}" not found.\n\nAvailable models:\n${e}`}}const r=await this.setHandler(e.sessionKey,o.id),d=`Model for this session switched to: ${o.name} (${o.id})`;return{text:r?`${d}\nSession reset. Starting a new conversation.`:d}}}export class DefaultModelCommand{registryProvider;setHandler;picoAgentProvider;name="defaultmodel";description="Set the default model for new sessions (usage: /defaultmodel <name or id>)";constructor(e,t,n){this.registryProvider=e,this.setHandler=t,this.picoAgentProvider=n}async execute(e){const t=e.args.trim();if(!t)return{text:"Usage: /defaultmodel <name or id>\nUse /models to see available models."};const n=this.picoAgentProvider?.(),i=filterEligibleModels(this.registryProvider(),n),s=t.toLowerCase();let o=i.find(e=>e.name===t)??i.find(e=>e.id===t)??i.find(e=>e.name.toLowerCase()===s)??i.find(e=>e.id.toLowerCase()===s);if(!o){const e=i.filter(e=>e.name.toLowerCase().includes(s)||e.id.toLowerCase().includes(s));if(1===e.length)o=e[0];else if(e.length>1){const n=e.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`"${t}" matches multiple models:\n${n}\n\nPlease be more specific.`}}}if(!o){const e=i.map(e=>` ${e.name} (${e.id})`).join("\n");return{text:`Model "${t}" not found.\n\nAvailable models:\n${e}`}}return await this.setHandler(o.id),{text:`Default model set to: ${o.name} (${o.id})\nNew sessions will use this model.`}}}
@@ -1,11 +1,13 @@
1
1
  import type { Command, CommandContext, CommandResult } from "./command.js";
2
- import type { ModelEntry } from "./model.js";
2
+ import type { ModelEntry, PicoAgentInfo } from "./model.js";
3
3
  export type ModelRegistryProvider = () => ModelEntry[];
4
+ export type PicoAgentProvider = () => PicoAgentInfo;
4
5
  export declare class ModelsCommand implements Command {
5
6
  private registryProvider;
7
+ private picoAgentProvider?;
6
8
  name: string;
7
9
  description: string;
8
- constructor(registryProvider: ModelRegistryProvider);
10
+ constructor(registryProvider: ModelRegistryProvider, picoAgentProvider?: PicoAgentProvider | undefined);
9
11
  execute(_ctx: CommandContext): Promise<CommandResult>;
10
12
  }
11
13
  //# sourceMappingURL=models.d.ts.map
@@ -1 +1 @@
1
- export class ModelsCommand{registryProvider;name="models";description="List configured models (usage: /models)";constructor(e){this.registryProvider=e}async execute(e){const r=this.registryProvider();if(0===r.length)return{text:"No models configured in the registry."};return{text:`Available models:\n${r.map(e=>{const r=e.proxy&&"not-used"!==e.proxy?" (proxied)":"";return` ${e.name} (${e.id})${r}`}).join("\n")}`}}}
1
+ import{filterEligibleModels as e}from"./model.js";export class ModelsCommand{registryProvider;picoAgentProvider;name="models";description="List configured models (usage: /models)";constructor(e,r){this.registryProvider=e,this.picoAgentProvider=r}async execute(r){const o=this.picoAgentProvider?.(),t=e(this.registryProvider(),o);if(0===t.length)return{text:"No models configured in the registry."};return{text:`Available models:\n${t.map(e=>{const r=e.proxy&&"not-used"!==e.proxy?" (proxied)":"";return` ${e.name} (${e.id})${r}`}).join("\n")}`}}}
@@ -1,7 +1,9 @@
1
1
  import type { Command, CommandContext, CommandResult } from "./command.js";
2
2
  export interface StatusInfo {
3
3
  agentModel: string;
4
+ agentModelName?: string;
4
5
  fallbackModel: string;
6
+ fallbackModelName?: string;
5
7
  coderSkill: boolean;
6
8
  showToolUse: boolean;
7
9
  subagentsEnabled: boolean;
@@ -1 +1 @@
1
- export class StatusCommand{provider;name="status";description="Show current server status (models, coder, nodes)";constructor(e){this.provider=e}async execute(e){const o=this.provider(e.sessionKey),s=[];if(s.push("Server Status"),s.push(` Agent model: ${o.agentModel}`),s.push(` Fallback model: ${o.fallbackModel||"(none)"}`),s.push(" Coder skill: "+(o.coderSkill?"on":"off")),s.push(" Show tool use: "+(o.showToolUse?"on":"off")),s.push(" SubAgents: "+(o.subagentsEnabled?"on":"off")),s.push(" Custom SubAgents: "+(o.customSubAgentsEnabled?"on":"off")),s.push(" Sandbox: "+(o.sandboxEnabled?"on":"off")),o.connectedNodes.length>0){s.push(` Connected nodes (${o.connectedNodes.length}):`);for(const e of o.connectedNodes){const o=e.displayName??e.nodeId,n=e.hostname?` (${e.hostname})`:"";s.push(` - ${o}${n}`)}}else s.push(" Connected nodes: none");return{text:s.join("\n")}}}
1
+ export class StatusCommand{provider;name="status";description="Show current server status (models, coder, nodes)";constructor(e){this.provider=e}async execute(e){const o=this.provider(e.sessionKey),s=[];s.push("Server Status");const n=o.agentModelName?`${o.agentModelName} (${o.agentModel})`:o.agentModel;if(s.push(` Agent model: ${n}`),o.fallbackModel){const e=o.fallbackModelName?`${o.fallbackModelName} (${o.fallbackModel})`:o.fallbackModel;s.push(` Fallback model: ${e}`)}else s.push(" Fallback model: (none)");if(s.push(" Coder skill: "+(o.coderSkill?"on":"off")),s.push(" Show tool use: "+(o.showToolUse?"on":"off")),s.push(" SubAgents: "+(o.subagentsEnabled?"on":"off")),s.push(" Custom SubAgents: "+(o.customSubAgentsEnabled?"on":"off")),s.push(" Sandbox: "+(o.sandboxEnabled?"on":"off")),o.connectedNodes.length>0){s.push(` Connected nodes (${o.connectedNodes.length}):`);for(const e of o.connectedNodes){const o=e.displayName??e.nodeId,n=e.hostname?` (${e.hostname})`:"";s.push(` - ${o}${n}`)}}else s.push(" Connected nodes: none");return{text:s.join("\n")}}}
package/dist/config.d.ts CHANGED
@@ -10,6 +10,30 @@ export declare function getNostromoKeyPath(): string;
10
10
  * Keeps up to 5 backups: .backup1 (oldest) → .backup5 (newest).
11
11
  */
12
12
  export declare function backupConfig(configPath: string): void;
13
+ declare const ModelEntrySchema: z.ZodObject<{
14
+ id: z.ZodString;
15
+ name: z.ZodString;
16
+ types: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEnum<{
17
+ internal: "internal";
18
+ external: "external";
19
+ "env-var": "env-var";
20
+ }>>>>;
21
+ proxy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
22
+ "not-used": "not-used";
23
+ direct: "direct";
24
+ proxied: "proxied";
25
+ }>>>;
26
+ fastUrl: z.ZodDefault<z.ZodOptional<z.ZodString>>;
27
+ fastProxyApiKey: z.ZodDefault<z.ZodOptional<z.ZodString>>;
28
+ apiKey: z.ZodDefault<z.ZodOptional<z.ZodString>>;
29
+ baseURL: z.ZodDefault<z.ZodOptional<z.ZodString>>;
30
+ useEnvVar: z.ZodDefault<z.ZodOptional<z.ZodString>>;
31
+ contextWindow: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
+ costInput: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
+ costOutput: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
34
+ costCacheRead: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
35
+ costCacheWrite: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
36
+ }, z.core.$strip>;
13
37
  declare const AppConfigSchema: z.ZodObject<{
14
38
  gmabPath: z.ZodDefault<z.ZodOptional<z.ZodString>>;
15
39
  host: z.ZodDefault<z.ZodOptional<z.ZodString>>;
@@ -90,6 +114,11 @@ declare const AppConfigSchema: z.ZodObject<{
90
114
  apiKey: z.ZodDefault<z.ZodOptional<z.ZodString>>;
91
115
  baseURL: z.ZodDefault<z.ZodOptional<z.ZodString>>;
92
116
  useEnvVar: z.ZodDefault<z.ZodOptional<z.ZodString>>;
117
+ contextWindow: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
118
+ costInput: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
119
+ costOutput: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
120
+ costCacheRead: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
121
+ costCacheWrite: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
93
122
  }, z.core.$strip>>>>>;
94
123
  stt: z.ZodDefault<z.ZodOptional<z.ZodObject<{
95
124
  enabled: z.ZodDefault<z.ZodBoolean>;
@@ -153,6 +182,18 @@ declare const AppConfigSchema: z.ZodObject<{
153
182
  agent: z.ZodDefault<z.ZodOptional<z.ZodObject<{
154
183
  model: z.ZodDefault<z.ZodString>;
155
184
  mainFallback: z.ZodDefault<z.ZodString>;
185
+ engine: z.ZodDefault<z.ZodOptional<z.ZodObject<{
186
+ type: z.ZodDefault<z.ZodEnum<{
187
+ claudecode: "claudecode";
188
+ pi: "pi";
189
+ }>>;
190
+ piModelRef: z.ZodDefault<z.ZodString>;
191
+ }, z.core.$loose>>>;
192
+ picoAgent: z.ZodDefault<z.ZodOptional<z.ZodObject<{
193
+ enabled: z.ZodDefault<z.ZodBoolean>;
194
+ modelRefs: z.ZodDefault<z.ZodArray<z.ZodString>>;
195
+ rollingMemoryModel: z.ZodDefault<z.ZodString>;
196
+ }, z.core.$strip>>>;
156
197
  maxTurns: z.ZodDefault<z.ZodNumber>;
157
198
  permissionMode: z.ZodDefault<z.ZodString>;
158
199
  sessionTTL: z.ZodDefault<z.ZodNumber>;
@@ -258,10 +299,26 @@ export declare function loadConfig(configPath?: string): AppConfig;
258
299
  */
259
300
  export declare function loadRawConfig(configPath?: string): any;
260
301
  /**
261
- * Resolve a model name to its actual model ID using the models registry.
262
- * If the name matches a registry entry, returns the entry's id.
263
- * Otherwise returns the raw value as-is (backwards compat / direct id usage).
302
+ * Resolve a model reference to its ModelEntry using the models registry.
303
+ *
304
+ * Accepts three formats (in priority order):
305
+ * 1. `name:id` — composite ref, matches on both name AND id
306
+ * 2. `name` only — backward compat, matches first entry by name
307
+ * 3. `id` only — fallback, matches first entry by id
308
+ *
309
+ * Returns the matching ModelEntry or undefined.
310
+ */
311
+ export declare function resolveModelEntry(config: AppConfig, ref: string): z.infer<typeof ModelEntrySchema> | undefined;
312
+ /**
313
+ * Extract the display name from a model reference.
314
+ * If ref is `name:id`, returns `name`. Otherwise returns the ref as-is.
315
+ */
316
+ export declare function modelRefName(ref: string): string;
317
+ /**
318
+ * Resolve a model reference to its actual model ID using the models registry.
319
+ * Accepts `name:id`, `name`, or raw `id`.
320
+ * Returns the entry's id if found, otherwise the raw value as-is.
264
321
  */
265
- export declare function resolveModelId(config: AppConfig, name: string): string;
322
+ export declare function resolveModelId(config: AppConfig, ref: string): string;
266
323
  export {};
267
324
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([])}),w=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),x).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),k=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),j=f.object({telegram:w.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),responses:k.optional().default({enabled:!0,port:3e3})}),M=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),S=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),R=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":M.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":S.optional().default({binaryPath:"whisper",model:"base"})}),T=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),C=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),A=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),U=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:T.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:C.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:A.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),z=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),D={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},I=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:z.optional().default(D)}),L=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),E=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default("")}),F=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:""},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:""},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:""}],K=f.array(E).default(F),_=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),q=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),G=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),L).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(q).default([]),plugins:f.array(_).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0)}),X={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},$={enabled:!0,recallStrategy:"builtin-only",dir:"",search:D},B={model:"claude-opus-4-6",mainFallback:"",maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0},W=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),V={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},H=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:W.optional().default(V)}),N={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:V},O=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),Q={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},Z=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:j.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:K.optional().default(F),stt:R.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:U.optional().default(X),memory:I.optional().default($),agent:G.optional().default(B),cron:H.optional().default(N),nostromo:O.optional().default(Q),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function J(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function Y(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(Y);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=Y(o);return t}return e}const ee={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:X,memory:$,agent:{...B,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:N,nostromo:Q};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ee});t(l,e,"utf-8")}const f=e(l,"utf-8");J(f);const p=Y(d(f)),m=Z.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=d(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return d(n)??{}}export function resolveModelId(e,t){if(!t)return t;const a=e.models?.find(e=>e.name===t);return a?a.id:t}
1
+ import{readFileSync as e,writeFileSync as t,existsSync as o,mkdirSync as a,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(a){if(o(a))try{const r=e=>`${a}.backup${e}`;o(r(1))&&l(r(1));for(let e=2;e<=5;e++)o(r(e))&&n(r(e),r(e-1));t(r(5),e(a)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([])}),w=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),x).default({})}),P=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),M=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),R=f.object({telegram:w.optional().default({enabled:!1,accounts:{}}),whatsapp:P.optional().default({enabled:!1,accounts:{}}),discord:P.optional().default({enabled:!1,accounts:{}}),slack:P.optional().default({enabled:!1,accounts:{}}),signal:P.optional().default({enabled:!1,accounts:{}}),msteams:P.optional().default({enabled:!1,accounts:{}}),googlechat:P.optional().default({enabled:!1,accounts:{}}),line:P.optional().default({enabled:!1,accounts:{}}),matrix:P.optional().default({enabled:!1,accounts:{}}),responses:M.optional().default({enabled:!0,port:3e3})}),j=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),k=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),C=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":j.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":k.optional().default({binaryPath:"whisper",model:"base"})}),S=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),T=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),A=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),I=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:S.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:T.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:A.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),U=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),z={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},D=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:U.optional().default(z)}),W=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),E=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),L=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],F=f.array(E).default(L),K=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),O=f.object({name:f.string(),description:f.string(),prompt:f.string(),model:f.enum(["sonnet","opus","haiku","inherit"]).default("inherit"),tools:f.array(f.string()).default(["Read","Write","Edit","Glob","Grep","WebSearch","WebFetch"]),expandContext:f.boolean().default(!1),enabled:f.boolean().default(!1)}),_=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),q={type:"claudecode",piModelRef:""},G=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),X={enabled:!1,modelRefs:[],rollingMemoryModel:""},$=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:_.optional().default(q),picoAgent:G.optional().default(X),maxTurns:f.number().default(50),permissionMode:f.string().default("bypassPermissions"),sessionTTL:f.number().default(3600),queueMode:f.enum(["queue","collect","steer"]).default("steer"),queueDebounceMs:f.number().default(1500),queueCap:f.number().default(20),queueDropPolicy:f.enum(["old","new","summarize"]).default("summarize"),allowedTools:f.array(f.string()).default([]),disallowedTools:f.array(f.string()).default([]),mcpServers:f.record(f.string(),W).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(O).default([]),plugins:f.array(K).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0)}),B={enabled:!1,provider:"openai",maxTextLength:4096,timeoutMs:3e4,edge:{voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"},openai:{modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"},elevenlabs:{modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"}},V={enabled:!0,recallStrategy:"builtin-only",dir:"",search:z},N={model:"claude-opus-4-6",mainFallback:"",engine:q,picoAgent:X,maxTurns:50,permissionMode:"bypassPermissions",sessionTTL:3600,queueMode:"steer",queueDebounceMs:1500,queueCap:20,queueDropPolicy:"summarize",allowedTools:[],disallowedTools:[],mcpServers:{},workspacePath:"./workspace",builtinCoderSkill:!1,settingSources:"project",customSubAgents:[],plugins:[],inflightTyping:!0,autoApproveTools:!0,autoRenew:0},H=f.object({enabled:f.boolean().default(!1),every:f.number().default(18e5),channel:f.string().default(""),chatId:f.string().default(""),message:f.string().default(""),ackMaxChars:f.number().default(300)}),Q={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},Z=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:H.optional().default(Q)}),J={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:Q},Y=f.object({enabled:f.boolean().default(!0),port:f.number().default(3001),basePath:f.string().default("/nostromo"),configCheckInterval:f.number().default(5),autoRestart:f.boolean().default(!0)}),ee={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},te=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:R.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:F.optional().default(L),stt:C.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:I.optional().default(B),memory:D.optional().default(V),agent:$.optional().default(N),cron:Z.optional().default(J),nostromo:Y.optional().default(ee),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function oe(e){const t=function(e){const t=new Set,o=/\$\{([^}]+)\}/g;let a;for(;null!==(a=o.exec(e));)t.add(a[1]);return Array.from(t)}(e),o=t.filter(e=>!process.env[e]);o.length>0&&b.warn(`Missing environment variables referenced in config: ${o.join(", ")}. Add them to .env or export them before starting.`)}function ae(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(ae);if(null!==e&&"object"==typeof e){const t={};for(const[o,a]of Object.entries(e))t[o]=ae(a);return t}return e}const ne={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:B,memory:V,agent:{...N,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:J,nostromo:ee};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(o(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!o(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ne});t(l,e,"utf-8")}const f=e(l,"utf-8");oe(f);const p=ae(d(f)),m=te.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),a(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const o=d(e(l,"utf-8"))??{};o.timezone=m.timezone,t(l,c(o),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){a(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),a(e.agent.workspacePath,{recursive:!0});const o=s(y,"cron");a(o,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(o,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const a=t??r(process.cwd(),"config.yaml");if(!o(a))return{};const n=e(a,"utf-8");return d(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const o=e.models;if(!o?.length)return;const a=t.indexOf(":");if(a>=0){const e=t.substring(0,a),n=t.substring(a+1);return o.find(t=>t.name===e&&t.id===n)}return o.find(e=>e.name===t)??o.find(e=>e.id===t)}export function modelRefName(e){if(!e)return e;const t=e.indexOf(":");return t>=0?e.substring(0,t):e}export function resolveModelId(e,t){if(!t)return t;const o=resolveModelEntry(e,t);return o?o.id:t}
@@ -1 +1 @@
1
- import{Bot as t,InputFile as e,InlineKeyboard as i}from"grammy";import{validateChannelUser as a}from"../../auth/auth-middleware.js";import{parseMediaLines as n}from"../../utils/media-response.js";import{markdownToTelegramHtmlChunks as o}from"../../utils/telegram-format.js";import{createLogger as s}from"../../utils/logger.js";const r=s("Telegram");export class TelegramChannel{name="telegram";bot;config;tokenDb;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(e,i,a=!0){this.config=e,this.tokenDb=i,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,a=String(e.from?.id??"unknown"),n=String(e.chat?.id??e.callbackQuery.message?.chat?.id??"unknown"),o=e.from?.username;e.answerCallbackQuery().catch(()=>{}),this.startTypingInterval(n);t({chatId:n,userId:a,channelName:"telegram",text:i,attachments:[],username:o,rawContext:e}).then(async t=>{t&&t.trim()&&await this.sendText(n,t)}).catch(t=>{r.error(`Error handling callback from ${a}: ${t}`)})}),r.info("Starting Telegram bot..."),this.bot.start({onStart:t=>{r.info(`Telegram bot started: @${t.username}`)}})}async sendText(t,e){const i=o(e,4096);for(const a of i)try{await this.bot.api.sendMessage(t,a,{parse_mode:"HTML"})}catch{const i=c(e,4096);for(const e of i)await this.bot.api.sendMessage(t,e);break}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,a){const n=new i;for(const t of a)t.url?n.url(t.text,t.url):n.text(t.text,t.callbackData??t.text),n.row();const s=o(e,4096);for(let i=0;i<s.length-1;i++)try{await this.bot.api.sendMessage(t,s[i],{parse_mode:"HTML"})}catch{await this.bot.api.sendMessage(t,e.slice(0,4096))}const r=s[s.length-1]??e;try{await this.bot.api.sendMessage(t,r,{parse_mode:"HTML",reply_markup:n})}catch{await this.bot.api.sendMessage(t,e.slice(0,4096),{reply_markup:n})}await this.resendTypingIfActive(t)}async sendAudio(t,i,a){const n=new e(i);a?await this.bot.api.sendVoice(t,n):await this.bot.api.sendAudio(t,n),await this.resendTypingIfActive(t)}async stop(){try{await this.bot.stop(),r.info("Telegram bot stopped"),await new Promise(t=>setTimeout(t,100))}catch(t){r.warn(`Error stopping Telegram bot: ${t}`)}}async handleIncoming(t,i){const o=String(t.from?.id??"unknown"),s=String(t.chat?.id??"unknown"),c=t.from?.username,l=a(this.tokenDb,o,"telegram",this.config.dmPolicy,this.config.allowFrom);if(!l.authorized)return r.warn(`Unauthorized message from ${o} (@${c})`),void await t.reply(l.reason??"Not authorized.");this.startTypingInterval(s);try{const a=await this.buildIncomingMessage(t,s,o,c),l=await i(a),{textParts:h,mediaEntries:d}=n(l);for(const i of d)try{const a=new e(i.path);i.asVoice?await t.replyWithVoice(a):await t.replyWithAudio(a)}catch(t){r.error(`Failed to send audio: ${t}`)}const p=h.join("\n").trim();p&&await this.sendChunked(t,p),await this.resendTypingIfActive(s)}catch(e){r.error(`Error handling message from ${o}: ${e}`),await t.reply("An error occurred while processing your message.").catch(()=>{})}}async buildIncomingMessage(t,e,i,a){const n=t.message,o=[];let s=n.text??n.caption??void 0;if(n.photo&&n.photo.length>0){const e=n.photo[n.photo.length-1];o.push({type:"image",mimeType:"image/jpeg",fileSize:e.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,e.file_id)})}return n.voice&&o.push({type:"voice",mimeType:n.voice.mime_type??"audio/ogg",duration:n.voice.duration,fileSize:n.voice.file_size,getBuffer:()=>this.downloadFile(t,n.voice.file_id)}),n.audio&&o.push({type:"audio",mimeType:n.audio.mime_type??"audio/mpeg",fileName:n.audio.file_name,duration:n.audio.duration,fileSize:n.audio.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.audio.file_id)}),n.document&&o.push({type:"document",mimeType:n.document.mime_type,fileName:n.document.file_name,fileSize:n.document.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.document.file_id)}),n.video&&o.push({type:"video",mimeType:n.video.mime_type??"video/mp4",fileName:n.video.file_name,duration:n.video.duration,fileSize:n.video.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.video.file_id)}),n.video_note&&o.push({type:"video_note",mimeType:"video/mp4",duration:n.video_note.duration,fileSize:n.video_note.file_size,getBuffer:()=>this.downloadFile(t,n.video_note.file_id)}),n.sticker&&o.push({type:"sticker",mimeType:n.sticker.is_animated?"application/x-tgsticker":n.sticker.is_video?"video/webm":"image/webp",metadata:{emoji:n.sticker.emoji,setName:n.sticker.set_name},getBuffer:()=>this.downloadFile(t,n.sticker.file_id)}),n.location&&o.push({type:"location",metadata:{latitude:n.location.latitude,longitude:n.location.longitude},getBuffer:async()=>Buffer.alloc(0)}),n.contact&&o.push({type:"contact",metadata:{phoneNumber:n.contact.phone_number,firstName:n.contact.first_name,lastName:n.contact.last_name,userId:n.contact.user_id},getBuffer:async()=>Buffer.alloc(0)}),{chatId:e,userId:i,channelName:"telegram",text:s,attachments:o,username:a,rawContext:t}}async downloadFile(t,e){const i=await t.api.getFile(e),a=`https://api.telegram.org/file/bot${this.config.botToken}/${i.file_path}`,n=await fetch(a);if(!n.ok)throw new Error(`Failed to download file: ${n.statusText}`);return Buffer.from(await n.arrayBuffer())}async sendChunked(t,e){const i=o(e,4096);for(const a of i)try{await t.reply(a,{parse_mode:"HTML"})}catch{const i=c(e,4096);for(const e of i)await t.reply(e);break}}}function c(t,e){if(t.length<=e)return[t];const i=[];let a=t;for(;a.length>0;){if(a.length<=e){i.push(a);break}let t=a.lastIndexOf("\n",e);t<=0&&(t=e),i.push(a.slice(0,t)),a=a.slice(t).trimStart()}return i}
1
+ import{Bot as t,InputFile as e,InlineKeyboard as i}from"grammy";import{validateChannelUser as a}from"../../auth/auth-middleware.js";import{parseMediaLines as n}from"../../utils/media-response.js";import{markdownToTelegramHtmlChunks as o}from"../../utils/telegram-format.js";import{createLogger as s}from"../../utils/logger.js";const r=s("Telegram");export class TelegramChannel{name="telegram";bot;config;tokenDb;typingIntervals=new Map;inflightTyping;inflightCount=new Map;constructor(e,i,a=!0){this.config=e,this.tokenDb=i,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,a=String(e.from?.id??"unknown"),n=String(e.chat?.id??e.callbackQuery.message?.chat?.id??"unknown"),o=e.from?.username;e.answerCallbackQuery().catch(()=>{}),this.startTypingInterval(n);t({chatId:n,userId:a,channelName:"telegram",text:i,attachments:[],username:o,rawContext:e}).then(async t=>{t&&t.trim()&&await this.sendText(n,t)}).catch(t=>{r.error(`Error handling callback from ${a}: ${t}`)})}),r.info("Starting Telegram bot..."),this.bot.start({onStart:t=>{r.info(`Telegram bot started: @${t.username}`)}}).catch(t=>{String(t).includes("Aborted delay")?r.debug("Telegram polling stopped"):r.error(`Telegram bot polling error: ${t}`)})}async sendText(t,e){const i=o(e,4096);for(const a of i)try{await this.bot.api.sendMessage(t,a,{parse_mode:"HTML"})}catch{const i=l(e,4096);for(const e of i)await this.bot.api.sendMessage(t,e);break}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,a){const n=new i;for(const t of a)t.url?n.url(t.text,t.url):n.text(t.text,t.callbackData??t.text),n.row();const s=o(e,4096);for(let i=0;i<s.length-1;i++)try{await this.bot.api.sendMessage(t,s[i],{parse_mode:"HTML"})}catch{await this.bot.api.sendMessage(t,e.slice(0,4096))}const r=s[s.length-1]??e;try{await this.bot.api.sendMessage(t,r,{parse_mode:"HTML",reply_markup:n})}catch{await this.bot.api.sendMessage(t,e.slice(0,4096),{reply_markup:n})}await this.resendTypingIfActive(t)}async sendAudio(t,i,a){const n=new e(i);a?await this.bot.api.sendVoice(t,n):await this.bot.api.sendAudio(t,n),await this.resendTypingIfActive(t)}async stop(){try{await this.bot.stop(),r.info("Telegram bot stopped"),await new Promise(t=>setTimeout(t,100))}catch(t){r.warn(`Error stopping Telegram bot: ${t}`)}}async handleIncoming(t,i){const o=String(t.from?.id??"unknown"),s=String(t.chat?.id??"unknown"),l=t.from?.username,c=a(this.tokenDb,o,"telegram",this.config.dmPolicy,this.config.allowFrom);if(!c.authorized)return r.warn(`Unauthorized message from ${o} (@${l})`),void await t.reply(c.reason??"Not authorized.");this.startTypingInterval(s);try{const a=await this.buildIncomingMessage(t,s,o,l),c=await i(a),{textParts:d,mediaEntries:h}=n(c);for(const i of h)try{const a=new e(i.path);i.asVoice?await t.replyWithVoice(a):await t.replyWithAudio(a)}catch(t){r.error(`Failed to send audio: ${t}`)}const p=d.join("\n").trim();p&&await this.sendChunked(t,p),await this.resendTypingIfActive(s)}catch(e){r.error(`Error handling message from ${o}: ${e}`),await t.reply("An error occurred while processing your message.").catch(()=>{})}}async buildIncomingMessage(t,e,i,a){const n=t.message,o=[];let s=n.text??n.caption??void 0;if(n.photo&&n.photo.length>0){const e=n.photo[n.photo.length-1];o.push({type:"image",mimeType:"image/jpeg",fileSize:e.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,e.file_id)})}return n.voice&&o.push({type:"voice",mimeType:n.voice.mime_type??"audio/ogg",duration:n.voice.duration,fileSize:n.voice.file_size,getBuffer:()=>this.downloadFile(t,n.voice.file_id)}),n.audio&&o.push({type:"audio",mimeType:n.audio.mime_type??"audio/mpeg",fileName:n.audio.file_name,duration:n.audio.duration,fileSize:n.audio.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.audio.file_id)}),n.document&&o.push({type:"document",mimeType:n.document.mime_type,fileName:n.document.file_name,fileSize:n.document.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.document.file_id)}),n.video&&o.push({type:"video",mimeType:n.video.mime_type??"video/mp4",fileName:n.video.file_name,duration:n.video.duration,fileSize:n.video.file_size,caption:n.caption,getBuffer:()=>this.downloadFile(t,n.video.file_id)}),n.video_note&&o.push({type:"video_note",mimeType:"video/mp4",duration:n.video_note.duration,fileSize:n.video_note.file_size,getBuffer:()=>this.downloadFile(t,n.video_note.file_id)}),n.sticker&&o.push({type:"sticker",mimeType:n.sticker.is_animated?"application/x-tgsticker":n.sticker.is_video?"video/webm":"image/webp",metadata:{emoji:n.sticker.emoji,setName:n.sticker.set_name},getBuffer:()=>this.downloadFile(t,n.sticker.file_id)}),n.location&&o.push({type:"location",metadata:{latitude:n.location.latitude,longitude:n.location.longitude},getBuffer:async()=>Buffer.alloc(0)}),n.contact&&o.push({type:"contact",metadata:{phoneNumber:n.contact.phone_number,firstName:n.contact.first_name,lastName:n.contact.last_name,userId:n.contact.user_id},getBuffer:async()=>Buffer.alloc(0)}),{chatId:e,userId:i,channelName:"telegram",text:s,attachments:o,username:a,rawContext:t}}async downloadFile(t,e){const i=await t.api.getFile(e),a=`https://api.telegram.org/file/bot${this.config.botToken}/${i.file_path}`,n=await fetch(a);if(!n.ok)throw new Error(`Failed to download file: ${n.statusText}`);return Buffer.from(await n.arrayBuffer())}async sendChunked(t,e){const i=o(e,4096);for(const a of i)try{await t.reply(a,{parse_mode:"HTML"})}catch{const i=l(e,4096);for(const e of i)await t.reply(e);break}}}function l(t,e){if(t.length<=e)return[t];const i=[];let a=t;for(;a.length>0;){if(a.length<=e){i.push(a);break}let t=a.lastIndexOf("\n",e);t<=0&&(t=e),i.push(a.slice(0,t)),a=a.slice(t).trimStart()}return i}