@hera-al/server 1.6.6 → 1.6.11
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.
- package/dist/agent/agent-service.d.ts +1 -0
- package/dist/agent/agent-service.js +1 -1
- package/dist/agent/session-agent.d.ts +4 -2
- package/dist/agent/session-agent.js +1 -1
- package/dist/agent/session-db.d.ts +2 -0
- package/dist/agent/session-db.js +1 -1
- package/dist/commands/command.d.ts +7 -0
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -1
- package/dist/config.d.ts +170 -1
- package/dist/config.js +1 -1
- package/dist/cron/cron-service.d.ts +36 -2
- package/dist/cron/cron-service.js +1 -1
- package/dist/cron/types.d.ts +6 -0
- package/dist/gateway/bridge.d.ts +1 -1
- package/dist/gateway/channel-manager.d.ts +1 -1
- package/dist/gateway/channel-manager.js +1 -1
- package/dist/gateway/channels/telegram/config-types.d.ts +93 -0
- package/dist/gateway/channels/telegram/config-types.js +1 -0
- package/dist/gateway/channels/telegram/edit-delete.d.ts +73 -0
- package/dist/gateway/channels/telegram/edit-delete.js +1 -0
- package/dist/gateway/channels/telegram/error-handling.d.ts +63 -0
- package/dist/gateway/channels/telegram/error-handling.js +1 -0
- package/dist/gateway/channels/{telegram.d.ts → telegram/index.d.ts} +23 -11
- package/dist/gateway/channels/telegram/index.js +1 -0
- package/dist/gateway/channels/telegram/inline-buttons.d.ts +60 -0
- package/dist/gateway/channels/telegram/inline-buttons.js +1 -0
- package/dist/gateway/channels/telegram/polls.d.ts +50 -0
- package/dist/gateway/channels/telegram/polls.js +1 -0
- package/dist/gateway/channels/telegram/reactions.d.ts +56 -0
- package/dist/gateway/channels/telegram/reactions.js +1 -0
- package/dist/gateway/channels/telegram/retry-policy.d.ts +28 -0
- package/dist/gateway/channels/telegram/retry-policy.js +1 -0
- package/dist/gateway/channels/telegram/send.d.ts +55 -0
- package/dist/gateway/channels/telegram/send.js +1 -0
- package/dist/gateway/channels/telegram/stickers.d.ts +96 -0
- package/dist/gateway/channels/telegram/stickers.js +1 -0
- package/dist/gateway/channels/telegram/thread-support.d.ts +99 -0
- package/dist/gateway/channels/telegram/thread-support.js +1 -0
- package/dist/gateway/channels/telegram/utils.d.ts +69 -0
- package/dist/gateway/channels/telegram/utils.js +1 -0
- package/dist/gateway/channels/webchat.d.ts +1 -1
- package/dist/gateway/channels/webchat.js +1 -1
- package/dist/nostromo/ui-js-agent.js +1 -1
- package/dist/pi-agent-provider/pi-query.js +1 -1
- package/dist/pi-agent-provider/pi-types.d.ts +4 -0
- package/dist/server.js +1 -1
- package/dist/tools/cron-tools.js +1 -1
- package/dist/tools/message-tools.js +1 -1
- package/dist/tools/telegram-actions-tools.d.ts +13 -0
- package/dist/tools/telegram-actions-tools.js +1 -0
- package/installationPkg/config.example.yaml +55 -1
- package/package.json +1 -1
- package/dist/gateway/channels/telegram.js +0 -1
|
@@ -19,6 +19,7 @@ export declare class AgentService {
|
|
|
19
19
|
private memoryToolsServer;
|
|
20
20
|
private browserToolsServer;
|
|
21
21
|
private picoToolsServer;
|
|
22
|
+
private telegramToolsServer;
|
|
22
23
|
private channelManager;
|
|
23
24
|
private showToolUseGetter;
|
|
24
25
|
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);
|
|
@@ -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{
|
|
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{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createLogger as n}from"../utils/logger.js";const i=n("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;telegramToolsServer=null;channelManager=null;showToolUseGetter=null;constructor(e,s,n,i,l,a,h,g,c,d,u){this.config=e,n&&(this.channelManager=n,this.telegramToolsServer=r(n,()=>this.config)),s&&(this.nodeToolsServer=t(s)),n&&a&&(this.messageToolsServer=o(n,()=>this.config,a)),i&&(this.serverToolsServer=i),l&&(this.cronToolsServer=l),h&&(this.ttsToolsServer=h),g&&(this.memoryToolsServer=g),d&&(this.browserToolsServer=d),u&&(this.picoToolsServer=u),c&&(this.showToolUseGetter=c)}async sendMessage(e,s,t,o,r,n,l,a,h,g){const c=this.getOrCreateAgent(e,t,o,r,n,l,a,h,g);if(n&&n!==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(n);if(d||u){i.info(`[${e}] Proxy config change detected (old=${d}, new=${u}), restarting session`),this.destroySession(e);const t=this.getOrCreateAgent(e,void 0,o,r,n,l,a,h,g);return await t.send(s)}await c.setModel(n)}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)){i.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,n,l,a,h,g);try{return await d.send(s)}catch(s){i.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 i.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),this.telegramToolsServer&&e.push(this.telegramToolsServer),e}getOrCreateAgent(e,t,o,r,n,i,l,a,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,l,a,this.ttsToolsServer??void 0,this.memoryToolsServer??void 0,this.browserToolsServer??void 0,h,this.picoToolsServer??void 0,this.telegramToolsServer??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),i.info(`Session agent destroyed: ${e}`))}destroyAll(){for(const[e,s]of this.agents)s.close(),i.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 i.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"}]}}}
|
|
@@ -99,7 +99,7 @@ export declare class SessionAgent {
|
|
|
99
99
|
private autoApproveTools;
|
|
100
100
|
private pendingPermission;
|
|
101
101
|
private pendingQuestion;
|
|
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);
|
|
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, telegramToolsServer?: unknown);
|
|
103
103
|
/**
|
|
104
104
|
* Resolve Pi provider configuration based on the active model.
|
|
105
105
|
*
|
|
@@ -120,8 +120,10 @@ export declare class SessionAgent {
|
|
|
120
120
|
/**
|
|
121
121
|
* Send a prompt and wait for the agent's response.
|
|
122
122
|
* Behavior depends on queueMode when the agent is already busy.
|
|
123
|
+
*
|
|
124
|
+
* Automatically retries on API Error 500 (up to 3 attempts with 2s delay).
|
|
123
125
|
*/
|
|
124
|
-
send(prompt: BuiltPrompt): Promise<AgentResult>;
|
|
126
|
+
send(prompt: BuiltPrompt, _retryCount?: number): Promise<AgentResult>;
|
|
125
127
|
interrupt(): Promise<boolean>;
|
|
126
128
|
setModel(model: string): Promise<void>;
|
|
127
129
|
close(): void;
|
|
@@ -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;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}}}
|
|
1
|
+
import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const T=a??i.agent.model;this.model=T?t(i,T):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const S=i.agent.settingSources;"user"===S?this.opts.settingSources=["user"]:"project"===S?this.opts.settingSources=["project"]:"both"===S&&(this.opts.settingSources=["user","project"]);const K=i.agent.mainFallback;K&&(this.opts.fallbackModel=t(i,K)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const x={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(x,i.agent.mcpServers),h&&(x["node-tools"]=h),u&&(x["message-tools"]=u),c&&(x["server-tools"]=c),d&&(x["cron-tools"]=d),f&&(x["tts-tools"]=f),y&&(x["memory-tools"]=y),$&&(x["browser-tools"]=$),v&&(x["pico-tools"]=v),w&&(x["telegram-actions"]=w),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"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const P=i.agent.plugins.filter(e=>e.enabled);P.length>0&&(this.opts.options={...this.opts.options,plugins:P.map(e=>({type:"local",path:e.path}))});const R=this.buildEnvForModel(this.model);this.opts.env=R.env,R.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.response.includes("API Error: 500")||i.fullResponse&&i.fullResponse.includes("API Error: 500");if(n&&t<3){const s=t+1;if(o.warn(`[${this.sessionKey}] API Error 500 detected, retrying (${s}/3)...`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/3)...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,2e3)),this.send(e,s)}if(n&&t>=3&&(o.error(`[${this.sessionKey}] API Error 500 persists after 3 retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e="API did not respond after 3 attempts. Please try again later.";this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...i}=s;this.currentResponse=JSON.stringify(i,null,2),o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.slice(0,200)}`)}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();s&&(this.currentSessionId?(o.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}}}
|
|
@@ -21,6 +21,8 @@ export declare class SessionDB {
|
|
|
21
21
|
/** Return sessions with an active SDK session whose last_activity is older than `maxAgeMs` milliseconds. */
|
|
22
22
|
listStaleSessions(maxAgeMs: number): SessionRecord[];
|
|
23
23
|
deleteSession(sessionKey: string): void;
|
|
24
|
+
/** Prune stale cron sessions (sessionKey starting with "cron:") older than maxAgeMs. Returns count of deleted rows. */
|
|
25
|
+
pruneStaleCronSessions(maxAgeMs: number): number;
|
|
24
26
|
close(): void;
|
|
25
27
|
}
|
|
26
28
|
//# sourceMappingURL=session-db.d.ts.map
|
package/dist/agent/session-db.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import s from"better-sqlite3";import{createLogger as e}from"../utils/logger.js";const i=e("SessionDB");function t(s){return{sessionKey:s.session_key,sdkSessionId:s.sdk_session_id,modelOverride:s.model_override,createdAt:s.created_at,lastActivity:s.last_activity}}export class SessionDB{db;constructor(e){this.db=new s(e),this.db.pragma("journal_mode = WAL"),this.initSchema(),this.migrate(),i.info(`Session database opened at ${e}`)}initSchema(){this.db.exec("\n CREATE TABLE IF NOT EXISTS sessions_map (\n session_key TEXT PRIMARY KEY NOT NULL,\n sdk_session_id TEXT,\n model_override TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n last_activity TEXT NOT NULL DEFAULT (datetime('now'))\n );\n ")}migrate(){this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get()&&(i.info("Migrating data from old 'sessions' table to 'sessions_map'..."),this.db.exec("\n INSERT OR IGNORE INTO sessions_map (session_key, sdk_session_id, model_override, created_at, last_activity)\n SELECT session_key, sdk_session_id, model_override, created_at, last_activity\n FROM sessions;\n "),this.db.exec("DROP TABLE sessions;"),i.info("Migration complete — old 'sessions' table dropped."))}getOrCreate(s){return this.db.prepare("INSERT INTO sessions_map (session_key)\n VALUES (?)\n ON CONFLICT(session_key) DO UPDATE SET\n last_activity = datetime('now')").run(s),this.getBySessionKey(s)}updateSdkSessionId(s,e){this.db.prepare("UPDATE sessions_map SET sdk_session_id = ?, last_activity = datetime('now') WHERE session_key = ?").run(e,s),i.info(`SDK session updated: ${s} -> ${e}`)}updateModelOverride(s,e){this.db.prepare("UPDATE sessions_map SET model_override = ?, last_activity = datetime('now') WHERE session_key = ?").run(e,s)}getBySessionKey(s){const e=this.db.prepare("SELECT * FROM sessions_map WHERE session_key = ?").get(s);return e?t(e):null}listSessions(){return this.db.prepare("SELECT * FROM sessions_map ORDER BY last_activity DESC").all().map(t)}clearSdkSessionId(s){this.db.prepare("UPDATE sessions_map SET sdk_session_id = NULL, last_activity = datetime('now') WHERE session_key = ?").run(s),i.info(`SDK session cleared (model preserved): ${s}`)}listStaleSessions(s){const e=new Date(Date.now()-s).toISOString().replace("T"," ").replace("Z","");return this.db.prepare("SELECT * FROM sessions_map WHERE sdk_session_id IS NOT NULL AND last_activity < ?").all(e).map(t)}deleteSession(s){this.db.prepare("DELETE FROM sessions_map WHERE session_key = ?").run(s),i.info(`Session deleted: ${s}`)}close(){this.db.close()}}
|
|
1
|
+
import s from"better-sqlite3";import{createLogger as e}from"../utils/logger.js";const i=e("SessionDB");function t(s){return{sessionKey:s.session_key,sdkSessionId:s.sdk_session_id,modelOverride:s.model_override,createdAt:s.created_at,lastActivity:s.last_activity}}export class SessionDB{db;constructor(e){this.db=new s(e),this.db.pragma("journal_mode = WAL"),this.initSchema(),this.migrate(),i.info(`Session database opened at ${e}`)}initSchema(){this.db.exec("\n CREATE TABLE IF NOT EXISTS sessions_map (\n session_key TEXT PRIMARY KEY NOT NULL,\n sdk_session_id TEXT,\n model_override TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n last_activity TEXT NOT NULL DEFAULT (datetime('now'))\n );\n ")}migrate(){this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get()&&(i.info("Migrating data from old 'sessions' table to 'sessions_map'..."),this.db.exec("\n INSERT OR IGNORE INTO sessions_map (session_key, sdk_session_id, model_override, created_at, last_activity)\n SELECT session_key, sdk_session_id, model_override, created_at, last_activity\n FROM sessions;\n "),this.db.exec("DROP TABLE sessions;"),i.info("Migration complete — old 'sessions' table dropped."))}getOrCreate(s){return this.db.prepare("INSERT INTO sessions_map (session_key)\n VALUES (?)\n ON CONFLICT(session_key) DO UPDATE SET\n last_activity = datetime('now')").run(s),this.getBySessionKey(s)}updateSdkSessionId(s,e){this.db.prepare("UPDATE sessions_map SET sdk_session_id = ?, last_activity = datetime('now') WHERE session_key = ?").run(e,s),i.info(`SDK session updated: ${s} -> ${e}`)}updateModelOverride(s,e){this.db.prepare("UPDATE sessions_map SET model_override = ?, last_activity = datetime('now') WHERE session_key = ?").run(e,s)}getBySessionKey(s){const e=this.db.prepare("SELECT * FROM sessions_map WHERE session_key = ?").get(s);return e?t(e):null}listSessions(){return this.db.prepare("SELECT * FROM sessions_map ORDER BY last_activity DESC").all().map(t)}clearSdkSessionId(s){this.db.prepare("UPDATE sessions_map SET sdk_session_id = NULL, last_activity = datetime('now') WHERE session_key = ?").run(s),i.info(`SDK session cleared (model preserved): ${s}`)}listStaleSessions(s){const e=new Date(Date.now()-s).toISOString().replace("T"," ").replace("Z","");return this.db.prepare("SELECT * FROM sessions_map WHERE sdk_session_id IS NOT NULL AND last_activity < ?").all(e).map(t)}deleteSession(s){this.db.prepare("DELETE FROM sessions_map WHERE session_key = ?").run(s),i.info(`Session deleted: ${s}`)}pruneStaleCronSessions(s){const e=new Date(Date.now()-s).toISOString().replace("T"," ").replace("Z",""),t=this.db.prepare("DELETE FROM sessions_map WHERE session_key LIKE 'cron:%' AND last_activity < ?").run(e).changes;return t>0&&i.info(`Pruned ${t} stale cron session(s) (older than ${Math.round(s/36e5)}h)`),t}close(){this.db.close()}}
|
|
@@ -5,6 +5,11 @@ export interface CommandContext {
|
|
|
5
5
|
userId: string;
|
|
6
6
|
args: string;
|
|
7
7
|
}
|
|
8
|
+
export interface InlineButton {
|
|
9
|
+
text: string;
|
|
10
|
+
callbackData?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
}
|
|
8
13
|
export interface CommandResult {
|
|
9
14
|
text: string;
|
|
10
15
|
/** If true, the server should reset the session after this command. */
|
|
@@ -13,6 +18,8 @@ export interface CommandResult {
|
|
|
13
18
|
resetAgent?: boolean;
|
|
14
19
|
/** If set, the server should re-send this text through the full handleMessage pipeline instead of returning text directly. */
|
|
15
20
|
passthrough?: string;
|
|
21
|
+
/** Optional inline buttons to attach to the message (array of button rows). */
|
|
22
|
+
buttons?: InlineButton[][];
|
|
16
23
|
}
|
|
17
24
|
export interface Command {
|
|
18
25
|
name: string;
|
package/dist/commands/help.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export declare class HelpCommand implements Command {
|
|
|
4
4
|
name: string;
|
|
5
5
|
description: string;
|
|
6
6
|
constructor(getSdkCommands: () => string[]);
|
|
7
|
-
execute(
|
|
7
|
+
execute(ctx: CommandContext): Promise<CommandResult>;
|
|
8
8
|
}
|
|
9
9
|
//# sourceMappingURL=help.d.ts.map
|
package/dist/commands/help.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export class HelpCommand{getSdkCommands;name="help";description="Show available commands";constructor(e){this.getSdkCommands=e}async execute(e){const
|
|
1
|
+
export class HelpCommand{getSdkCommands;name="help";description="Show available commands (usage: /help [page])";constructor(e){this.getSdkCommands=e}async execute(e){const t=[{category:"Session",command:"/stop",description:"Interrupt the agent if it is currently processing"},{category:"Session",command:"/new",description:"Start a fresh session (clears conversation history and memory)"},{category:"Session",command:"/compact",description:"Compress the current session context to reduce token usage"},{category:"Options",command:"/usage",description:"Show SDK usage for the current session"},{category:"Options",command:"/sandbox on|off",description:"Toggle sandbox mode for command execution"},{category:"Options",command:"/showtool on|off",description:"Show tool-use notifications in the chat"},{category:"Options",command:"/subagents on|off",description:"Enable or disable subagent invocation"},{category:"Models",command:"/model <name>",description:"Switch the agent model for the current session"},{category:"Models",command:"/models",description:"List all models in the registry"},{category:"Models",command:"/defaultmodel <name>",description:"Set the default model for new sessions"},{category:"Skills",command:"/coder on|off",description:"Toggle the built-in coder skill in the system prompt"},{category:"Skills",command:"/customsubagents on|off",description:"Pass custom subagents (configured in Nostromo) to the agent"},{category:"Skills",command:"/mcp",description:"List available MCP skills"},{category:"Info",command:"/help",description:"Show this message"},{category:"Info",command:"/status",description:"Show current server state (model, fallback, toggles)"}],o=["compact","keybindings-help"],s=this.getSdkCommands().filter(e=>!o.includes(e)&&!e.startsWith("mcp__")).sort((e,t)=>e.localeCompare(t));for(const e of s)t.push({category:"Claude SDK",command:`/${e}`,description:""});const a=e.args.trim(),n=a?Math.max(1,parseInt(a,10)):1,c=Math.ceil(t.length/8),i=Math.min(n,c),r=8*(i-1),l=Math.min(r+8,t.length),m=t.slice(r,l),d=[];d.push(`Commands (${i}/${c})`),d.push("");const p=t=>"whatsapp"===e.channelName?`*${t}*`:`**${t}**`;let h,g="";for(const e of m){e.category!==g&&(""!==g&&d.push(""),d.push(p(e.category)),g=e.category);const t=e.description?` - ${e.description}`:"";d.push(` ${e.command}${t}`)}if(c>1&&(d.push(""),d.push(`Use /help <page> to see other pages (1-${c})`)),"telegram"===e.channelName&&(h=[[{text:"📊 Status",callbackData:"/status"},{text:"🔄 New",callbackData:"/new"}],[{text:"🤖 Models",callbackData:"/models"},{text:"📝 Usage",callbackData:"/usage"}],[{text:"🧩 MCP",callbackData:"/mcp"},{text:"💻 Coder",callbackData:"/coder on"}]],c>1)){const e=[];i>1&&e.push({text:"◀️ Prev",callbackData:"/help "+(i-1)}),e.push({text:`${i}/${c}`,callbackData:`/help ${i}`}),i<c&&e.push({text:"Next ▶️",callbackData:`/help ${i+1}`}),h.push(e)}return{text:d.join("\n"),buttons:h}}}
|
|
@@ -8,6 +8,6 @@ export declare class ModelsCommand implements Command {
|
|
|
8
8
|
name: string;
|
|
9
9
|
description: string;
|
|
10
10
|
constructor(registryProvider: ModelRegistryProvider, picoAgentProvider?: PicoAgentProvider | undefined);
|
|
11
|
-
execute(
|
|
11
|
+
execute(ctx: CommandContext): Promise<CommandResult>;
|
|
12
12
|
}
|
|
13
13
|
//# sourceMappingURL=models.d.ts.map
|
package/dist/commands/models.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{filterEligibleModels as e}from"./model.js";export class ModelsCommand{registryProvider;picoAgentProvider;name="models";description="List configured models (usage: /models)";constructor(e,
|
|
1
|
+
import{filterEligibleModels as e}from"./model.js";export class ModelsCommand{registryProvider;picoAgentProvider;name="models";description="List configured models (usage: /models [page])";constructor(e,t){this.registryProvider=e,this.picoAgentProvider=t}async execute(t){const s=this.picoAgentProvider?.(),o=e(this.registryProvider(),s);if(0===o.length)return{text:"No models configured in the registry."};const a=t.args.trim(),r=a?Math.max(1,parseInt(a,10)):1,n=Math.ceil(o.length/10),i=Math.min(r,n),l=10*(i-1),c=Math.min(l+10,o.length),m=o.slice(l,c);let d,g=(n>1?`Available models (page ${i}/${n}):\n`:"Available models:\n")+m.map(e=>{const t=e.proxy&&"not-used"!==e.proxy?" (proxied)":"";return` ${e.name} (${e.id})${t}`}).join("\n");if(n>1&&(g+=`\n\nUse /models <page> to see other pages (1-${n})`),"telegram"===t.channelName){const e=m.map(e=>({text:e.name.length>25?e.name.substring(0,22)+"...":e.name,callbackData:`/model ${e.id}`}));d=[];for(let t=0;t<e.length;t+=2){const s=e.slice(t,t+2);d.push(s)}if(n>1){const e=[];i>1&&e.push({text:"◀️ Prev",callbackData:"/models "+(i-1)}),e.push({text:`${i}/${n}`,callbackData:`/models ${i}`}),i<n&&e.push({text:"Next ▶️",callbackData:`/models ${i+1}`}),d.push(e)}}return{text:g,buttons:d}}}
|
package/dist/config.d.ts
CHANGED
|
@@ -10,6 +10,134 @@ 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 TelegramActionConfigSchema: z.ZodObject<{
|
|
14
|
+
reactions: z.ZodDefault<z.ZodBoolean>;
|
|
15
|
+
sendMessage: z.ZodDefault<z.ZodBoolean>;
|
|
16
|
+
editMessage: z.ZodDefault<z.ZodBoolean>;
|
|
17
|
+
deleteMessage: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
sticker: z.ZodDefault<z.ZodBoolean>;
|
|
19
|
+
createForumTopic: z.ZodDefault<z.ZodBoolean>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
declare const TelegramRetryConfigSchema: z.ZodObject<{
|
|
22
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
baseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
24
|
+
maxDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
declare const TelegramInlineButtonsScopeSchema: z.ZodEnum<{
|
|
27
|
+
off: "off";
|
|
28
|
+
dm: "dm";
|
|
29
|
+
group: "group";
|
|
30
|
+
all: "all";
|
|
31
|
+
allowlist: "allowlist";
|
|
32
|
+
}>;
|
|
33
|
+
declare const TelegramAccountSchema: z.ZodObject<{
|
|
34
|
+
botToken: z.ZodString;
|
|
35
|
+
dmPolicy: z.ZodDefault<z.ZodEnum<{
|
|
36
|
+
allowlist: "allowlist";
|
|
37
|
+
open: "open";
|
|
38
|
+
token: "token";
|
|
39
|
+
}>>;
|
|
40
|
+
allowFrom: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
41
|
+
name: z.ZodOptional<z.ZodString>;
|
|
42
|
+
reactionLevel: z.ZodDefault<z.ZodEnum<{
|
|
43
|
+
off: "off";
|
|
44
|
+
ack: "ack";
|
|
45
|
+
minimal: "minimal";
|
|
46
|
+
extensive: "extensive";
|
|
47
|
+
}>>;
|
|
48
|
+
reactionNotifications: z.ZodDefault<z.ZodEnum<{
|
|
49
|
+
off: "off";
|
|
50
|
+
all: "all";
|
|
51
|
+
own: "own";
|
|
52
|
+
}>>;
|
|
53
|
+
inlineButtonsScope: z.ZodOptional<z.ZodEnum<{
|
|
54
|
+
off: "off";
|
|
55
|
+
dm: "dm";
|
|
56
|
+
group: "group";
|
|
57
|
+
all: "all";
|
|
58
|
+
allowlist: "allowlist";
|
|
59
|
+
}>>;
|
|
60
|
+
textChunkLimit: z.ZodDefault<z.ZodNumber>;
|
|
61
|
+
streamMode: z.ZodDefault<z.ZodEnum<{
|
|
62
|
+
off: "off";
|
|
63
|
+
partial: "partial";
|
|
64
|
+
block: "block";
|
|
65
|
+
}>>;
|
|
66
|
+
linkPreview: z.ZodDefault<z.ZodBoolean>;
|
|
67
|
+
actions: z.ZodOptional<z.ZodObject<{
|
|
68
|
+
reactions: z.ZodDefault<z.ZodBoolean>;
|
|
69
|
+
sendMessage: z.ZodDefault<z.ZodBoolean>;
|
|
70
|
+
editMessage: z.ZodDefault<z.ZodBoolean>;
|
|
71
|
+
deleteMessage: z.ZodDefault<z.ZodBoolean>;
|
|
72
|
+
sticker: z.ZodDefault<z.ZodBoolean>;
|
|
73
|
+
createForumTopic: z.ZodDefault<z.ZodBoolean>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
retry: z.ZodOptional<z.ZodObject<{
|
|
76
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
77
|
+
baseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
78
|
+
maxDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
79
|
+
}, z.core.$strip>>;
|
|
80
|
+
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
proxy: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
declare const TelegramConfigSchema: z.ZodObject<{
|
|
84
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
85
|
+
accounts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
86
|
+
botToken: z.ZodString;
|
|
87
|
+
dmPolicy: z.ZodDefault<z.ZodEnum<{
|
|
88
|
+
allowlist: "allowlist";
|
|
89
|
+
open: "open";
|
|
90
|
+
token: "token";
|
|
91
|
+
}>>;
|
|
92
|
+
allowFrom: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
93
|
+
name: z.ZodOptional<z.ZodString>;
|
|
94
|
+
reactionLevel: z.ZodDefault<z.ZodEnum<{
|
|
95
|
+
off: "off";
|
|
96
|
+
ack: "ack";
|
|
97
|
+
minimal: "minimal";
|
|
98
|
+
extensive: "extensive";
|
|
99
|
+
}>>;
|
|
100
|
+
reactionNotifications: z.ZodDefault<z.ZodEnum<{
|
|
101
|
+
off: "off";
|
|
102
|
+
all: "all";
|
|
103
|
+
own: "own";
|
|
104
|
+
}>>;
|
|
105
|
+
inlineButtonsScope: z.ZodOptional<z.ZodEnum<{
|
|
106
|
+
off: "off";
|
|
107
|
+
dm: "dm";
|
|
108
|
+
group: "group";
|
|
109
|
+
all: "all";
|
|
110
|
+
allowlist: "allowlist";
|
|
111
|
+
}>>;
|
|
112
|
+
textChunkLimit: z.ZodDefault<z.ZodNumber>;
|
|
113
|
+
streamMode: z.ZodDefault<z.ZodEnum<{
|
|
114
|
+
off: "off";
|
|
115
|
+
partial: "partial";
|
|
116
|
+
block: "block";
|
|
117
|
+
}>>;
|
|
118
|
+
linkPreview: z.ZodDefault<z.ZodBoolean>;
|
|
119
|
+
actions: z.ZodOptional<z.ZodObject<{
|
|
120
|
+
reactions: z.ZodDefault<z.ZodBoolean>;
|
|
121
|
+
sendMessage: z.ZodDefault<z.ZodBoolean>;
|
|
122
|
+
editMessage: z.ZodDefault<z.ZodBoolean>;
|
|
123
|
+
deleteMessage: z.ZodDefault<z.ZodBoolean>;
|
|
124
|
+
sticker: z.ZodDefault<z.ZodBoolean>;
|
|
125
|
+
createForumTopic: z.ZodDefault<z.ZodBoolean>;
|
|
126
|
+
}, z.core.$strip>>;
|
|
127
|
+
retry: z.ZodOptional<z.ZodObject<{
|
|
128
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
129
|
+
baseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
130
|
+
maxDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
133
|
+
proxy: z.ZodOptional<z.ZodString>;
|
|
134
|
+
}, z.core.$strip>>>;
|
|
135
|
+
}, z.core.$strip>;
|
|
136
|
+
export type TelegramActionConfig = z.infer<typeof TelegramActionConfigSchema>;
|
|
137
|
+
export type TelegramRetryConfig = z.infer<typeof TelegramRetryConfigSchema>;
|
|
138
|
+
export type TelegramInlineButtonsScope = z.infer<typeof TelegramInlineButtonsScopeSchema>;
|
|
139
|
+
export type TelegramAccountConfig = z.infer<typeof TelegramAccountSchema>;
|
|
140
|
+
export type TelegramConfig = z.infer<typeof TelegramConfigSchema>;
|
|
13
141
|
declare const ModelEntrySchema: z.ZodObject<{
|
|
14
142
|
id: z.ZodString;
|
|
15
143
|
name: z.ZodString;
|
|
@@ -52,11 +180,52 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
52
180
|
accounts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
53
181
|
botToken: z.ZodString;
|
|
54
182
|
dmPolicy: z.ZodDefault<z.ZodEnum<{
|
|
183
|
+
allowlist: "allowlist";
|
|
55
184
|
open: "open";
|
|
56
185
|
token: "token";
|
|
57
|
-
allowlist: "allowlist";
|
|
58
186
|
}>>;
|
|
59
187
|
allowFrom: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
188
|
+
name: z.ZodOptional<z.ZodString>;
|
|
189
|
+
reactionLevel: z.ZodDefault<z.ZodEnum<{
|
|
190
|
+
off: "off";
|
|
191
|
+
ack: "ack";
|
|
192
|
+
minimal: "minimal";
|
|
193
|
+
extensive: "extensive";
|
|
194
|
+
}>>;
|
|
195
|
+
reactionNotifications: z.ZodDefault<z.ZodEnum<{
|
|
196
|
+
off: "off";
|
|
197
|
+
all: "all";
|
|
198
|
+
own: "own";
|
|
199
|
+
}>>;
|
|
200
|
+
inlineButtonsScope: z.ZodOptional<z.ZodEnum<{
|
|
201
|
+
off: "off";
|
|
202
|
+
dm: "dm";
|
|
203
|
+
group: "group";
|
|
204
|
+
all: "all";
|
|
205
|
+
allowlist: "allowlist";
|
|
206
|
+
}>>;
|
|
207
|
+
textChunkLimit: z.ZodDefault<z.ZodNumber>;
|
|
208
|
+
streamMode: z.ZodDefault<z.ZodEnum<{
|
|
209
|
+
off: "off";
|
|
210
|
+
partial: "partial";
|
|
211
|
+
block: "block";
|
|
212
|
+
}>>;
|
|
213
|
+
linkPreview: z.ZodDefault<z.ZodBoolean>;
|
|
214
|
+
actions: z.ZodOptional<z.ZodObject<{
|
|
215
|
+
reactions: z.ZodDefault<z.ZodBoolean>;
|
|
216
|
+
sendMessage: z.ZodDefault<z.ZodBoolean>;
|
|
217
|
+
editMessage: z.ZodDefault<z.ZodBoolean>;
|
|
218
|
+
deleteMessage: z.ZodDefault<z.ZodBoolean>;
|
|
219
|
+
sticker: z.ZodDefault<z.ZodBoolean>;
|
|
220
|
+
createForumTopic: z.ZodDefault<z.ZodBoolean>;
|
|
221
|
+
}, z.core.$strip>>;
|
|
222
|
+
retry: z.ZodOptional<z.ZodObject<{
|
|
223
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
224
|
+
baseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
225
|
+
maxDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
226
|
+
}, z.core.$strip>>;
|
|
227
|
+
timeoutSeconds: z.ZodOptional<z.ZodNumber>;
|
|
228
|
+
proxy: z.ZodOptional<z.ZodString>;
|
|
60
229
|
}, z.core.$strip>>>;
|
|
61
230
|
}, z.core.$strip>>>;
|
|
62
231
|
whatsapp: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
+
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 i}from"node:path";import{homedir as s}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("~",s()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=i(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return i(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({reactions:f.boolean().default(!0),sendMessage:f.boolean().default(!0),editMessage:f.boolean().default(!0),deleteMessage:f.boolean().default(!0),sticker:f.boolean().default(!1),createForumTopic:f.boolean().default(!1)}),w=f.object({maxAttempts:f.number().default(3),baseDelayMs:f.number().default(1e3),maxDelayMs:f.number().default(3e4)}),M=f.enum(["off","dm","group","all","allowlist"]),k=f.object({botToken:f.string(),dmPolicy:f.enum(["open","token","allowlist"]).default("allowlist"),allowFrom:f.array(f.union([f.string(),f.number()])).default([]),name:f.string().optional(),reactionLevel:f.enum(["off","ack","minimal","extensive"]).default("ack"),reactionNotifications:f.enum(["off","own","all"]).default("off"),inlineButtonsScope:M.optional(),textChunkLimit:f.number().default(4e3),streamMode:f.enum(["off","partial","block"]).default("partial"),linkPreview:f.boolean().default(!0),actions:x.optional(),retry:w.optional(),timeoutSeconds:f.number().optional(),proxy:f.string().optional()}),P=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),j=f.object({enabled:f.boolean().default(!1),accounts:v.default({})}),R=f.object({enabled:f.boolean().default(!0),port:f.number().default(3004)}),C=f.object({telegram:P.optional().default({enabled:!1,accounts:{}}),whatsapp:j.optional().default({enabled:!1,accounts:{}}),discord:j.optional().default({enabled:!1,accounts:{}}),slack:j.optional().default({enabled:!1,accounts:{}}),signal:j.optional().default({enabled:!1,accounts:{}}),msteams:j.optional().default({enabled:!1,accounts:{}}),googlechat:j.optional().default({enabled:!1,accounts:{}}),line:j.optional().default({enabled:!1,accounts:{}}),matrix:j.optional().default({enabled:!1,accounts:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),I=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),D=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:I.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:D.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W)}),F=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),K=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),O=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],_=f.array(K).default(O),q=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),B=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({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),X={type:"claudecode",piModelRef:""},$=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),N={enabled:!1,modelRefs:[],rollingMemoryModel:""},V=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:G.optional().default(X),picoAgent:$.optional().default(N),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(),F).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(B).default([]),plugins:f.array(q).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0)}),H={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"}},Q={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W},Z={model:"claude-opus-4-6",mainFallback:"",engine:X,picoAgent:N,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},J=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)}),Y={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},ee=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:J.optional().default(Y)}),te={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:Y},oe=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)}),ae={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ne=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:_.optional().default(O),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(H),memory:E.optional().default(Q),agent:V.optional().default(Z),cron:ee.optional().default(te),nostromo:oe.optional().default(ae),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function le(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 re(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(re);if(null!==e&&"object"==typeof e){const t={};for(const[o,a]of Object.entries(e))t[o]=re(a);return t}return e}const ie={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:H,memory:Q,agent:{...Z,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:te,nostromo:ae};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),s=r(process.cwd(),".env");if(o(s)&&(u({path:s}),b.info(`Loaded .env from ${s}`)),!o(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ie});t(l,e,"utf-8")}const f=e(l,"utf-8");le(f);const p=re(d(f)),m=ne.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=i(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=i(y,"cron");a(o,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):i(o,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:i(y,"core.db"),memoryDir:i(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}
|
|
@@ -8,6 +8,10 @@ export type CronServiceOpts = {
|
|
|
8
8
|
enabled: boolean;
|
|
9
9
|
defaultTimezone?: string;
|
|
10
10
|
onExecute: (job: CronJob) => Promise<CronExecuteResult>;
|
|
11
|
+
/** Optional: prune stale cron sessions from SessionDB. */
|
|
12
|
+
sessionReaper?: {
|
|
13
|
+
pruneStaleSessions: (maxAgeMs: number) => number;
|
|
14
|
+
};
|
|
11
15
|
};
|
|
12
16
|
export type CronStatusSummary = {
|
|
13
17
|
enabled: boolean;
|
|
@@ -20,22 +24,46 @@ export declare class CronService {
|
|
|
20
24
|
private timer;
|
|
21
25
|
private running;
|
|
22
26
|
private op;
|
|
27
|
+
private lastReaperSweepAtMs;
|
|
23
28
|
private readonly storePath;
|
|
24
29
|
private readonly enabled;
|
|
25
30
|
private readonly defaultTimezone;
|
|
26
31
|
private readonly onExecute;
|
|
32
|
+
private readonly sessionReaper?;
|
|
27
33
|
constructor(opts: CronServiceOpts);
|
|
28
34
|
private locked;
|
|
29
35
|
private ensureLoaded;
|
|
30
36
|
private persist;
|
|
37
|
+
private computeStaggeredCronNextRunAtMs;
|
|
31
38
|
private computeJobNextRunAtMs;
|
|
39
|
+
/**
|
|
40
|
+
* Safe wrapper for schedule computation. Returns `true` if value changed.
|
|
41
|
+
* On error, increments scheduleErrorCount and auto-disables after MAX_SCHEDULE_ERRORS.
|
|
42
|
+
*/
|
|
43
|
+
private safeRecomputeJobNextRunAtMs;
|
|
44
|
+
/**
|
|
45
|
+
* Full recompute: recomputes nextRunAtMs for all enabled jobs.
|
|
46
|
+
* Use only at start() when we need a clean baseline.
|
|
47
|
+
*/
|
|
32
48
|
private recomputeNextRuns;
|
|
49
|
+
/**
|
|
50
|
+
* Maintenance-only recompute: handles disabled jobs and stuck markers,
|
|
51
|
+
* but does NOT recompute nextRunAtMs for enabled jobs that already have a value.
|
|
52
|
+
* Prevents silently advancing past-due nextRunAtMs without execution.
|
|
53
|
+
* (OpenClaw #13992, #17852)
|
|
54
|
+
*/
|
|
55
|
+
private recomputeNextRunsForMaintenance;
|
|
33
56
|
private nextWakeAtMs;
|
|
34
57
|
private armTimer;
|
|
35
58
|
private stopTimer;
|
|
36
59
|
private onTimer;
|
|
37
|
-
private
|
|
38
|
-
|
|
60
|
+
private findDueJobs;
|
|
61
|
+
/** Resolve the effective timeout for a job (per-job override or global default). */
|
|
62
|
+
private resolveJobTimeoutMs;
|
|
63
|
+
/** Execute onExecute with a safety-net timeout (OpenClaw DEFAULT_JOB_TIMEOUT_MS). */
|
|
64
|
+
private executeWithTimeout;
|
|
65
|
+
private applyJobResult;
|
|
66
|
+
private sweepStaleSessions;
|
|
39
67
|
start(): Promise<void>;
|
|
40
68
|
stop(): void;
|
|
41
69
|
status(): Promise<CronStatusSummary>;
|
|
@@ -53,5 +81,11 @@ export declare class CronService {
|
|
|
53
81
|
ran: boolean;
|
|
54
82
|
reason?: string;
|
|
55
83
|
}>;
|
|
84
|
+
/**
|
|
85
|
+
* After restart, find and execute jobs that were due during downtime.
|
|
86
|
+
* Skips one-shot ("at") jobs that already ran at least once.
|
|
87
|
+
* (OpenClaw runMissedJobs)
|
|
88
|
+
*/
|
|
89
|
+
private runMissedJobs;
|
|
56
90
|
}
|
|
57
91
|
//# sourceMappingURL=cron-service.d.ts.map
|