@hera-al/server 1.6.40 → 1.6.43
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.js +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +1 -1
- package/dist/gateway/bridge.d.ts +2 -2
- package/dist/gateway/channel-manager.d.ts +0 -7
- package/dist/gateway/channel-manager.js +1 -1
- package/dist/gateway/channels/mesh.d.ts +23 -2
- package/dist/gateway/channels/mesh.js +1 -1
- package/dist/gateway/channels/telegram/index.d.ts +4 -10
- package/dist/gateway/channels/telegram/index.js +1 -1
- package/dist/gateway/channels/webchat.d.ts +3 -6
- package/dist/gateway/channels/webchat.js +1 -1
- package/dist/gateway/channels/whatsapp.d.ts +3 -9
- package/dist/gateway/channels/whatsapp.js +1 -1
- package/dist/gateway/typing-coordinator.d.ts +43 -0
- package/dist/gateway/typing-coordinator.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -1
- package/dist/tools/message-tools.d.ts +1 -1
- package/dist/tools/message-tools.js +1 -1
- package/dist/tools/server-tools.d.ts +11 -1
- package/dist/tools/server-tools.js +1 -1
- package/installationPkg/AGENTS.md +4 -0
- package/installationPkg/BEHAVIOUR.md +95 -76
- package/installationPkg/config.example.yaml +31 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createLogger as l}from"../utils/logger.js";const c=l("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=
|
|
1
|
+
import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as i}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as n}from"../tools/dynamic-ui-tools.js";import{createStateToolsServer as a}from"../tools/state-tools.js";import{createLogger as l}from"../utils/logger.js";const c=l("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>i({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>n({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=s=>e(a,()=>this.config,h,s)),l&&(this.serverToolsFactory=l),c&&(this.cronToolsFactory=c),g&&(this.ttsToolsFactory=g),y&&(this.memoryToolsFactory=y),u&&(this.browserToolsFactory=u),T&&(this.picoToolsFactory=T),m&&(this.plasmaClientToolsFactory=m),F&&(this.conceptToolsFactory=F),d&&(this.showToolUseGetter=d)}async sendMessage(s,o,t,e,r,i,n,a,l,h){const g=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);if(i&&i!==g.getModel()){const t=s=>{const o=this.config.models.find(o=>o.id===s);return!(!o?.proxy||"not-used"===o.proxy)},y=t(g.getModel()),d=t(i);if(y||d){c.info(`[${s}] Proxy config change detected (old=${y}, new=${d}), restarting session`),this.destroySession(s);const t=this.getOrCreateAgent(s,void 0,e,r,i,n,a,l,h);return await t.send(o)}await g.setModel(i)}try{return await g.send(o)}catch(y){const d=y instanceof Error?y.message:String(y);if(d.includes("INVALID_ARGUMENT")||/API Error: 400/.test(d)){c.warn(`[${s}] Transient API 400 error, retrying once: ${d.slice(0,120)}`),this.agents.delete(s),g.close();const y=this.getOrCreateAgent(s,t,e,r,i,n,a,l,h);try{return await y.send(o)}catch(o){c.error(`[${s}] Retry also failed: ${o}`),this.agents.delete(s),y.close();const e=o instanceof Error?o.message:String(o);if(t)return{response:e.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw o}}if(this.agents.delete(s),g.close(),t)return c.warn(`Session agent failed for ${s}: ${y}`),{response:d.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw y}}hasNodeTools(){return null!==this.nodeToolsFactory}hasMessageTools(){return null!==this.messageToolsFactory}getToolServers(){if(!this.refToolServers){const s=[];this.nodeToolsFactory&&s.push(this.nodeToolsFactory()),this.messageToolsFactory&&s.push(this.messageToolsFactory()),this.serverToolsFactory&&s.push(this.serverToolsFactory()),this.cronToolsFactory&&s.push(this.cronToolsFactory()),this.ttsToolsFactory&&s.push(this.ttsToolsFactory()),this.memoryToolsFactory&&s.push(this.memoryToolsFactory()),this.browserToolsFactory&&s.push(this.browserToolsFactory()),this.picoToolsFactory&&s.push(this.picoToolsFactory()),this.telegramToolsFactory&&s.push(this.telegramToolsFactory()),this.a2uiToolsFactory&&s.push(this.a2uiToolsFactory()),this.dynamicUIToolsFactory&&s.push(this.dynamicUIToolsFactory()),this.plasmaClientToolsFactory&&s.push(this.plasmaClientToolsFactory()),this.conceptToolsFactory&&s.push(this.conceptToolsFactory()),s.push(a("__ref__",this.config.dataDir)),this.refToolServers=s}return this.refToolServers}getOrCreateAgent(s,t,e,r,i,l,c,h,g){const y=this.agents.get(s);if(y&&y.isActive())return y;let d,u;y&&(y.close(),this.agents.delete(s));const T=s.indexOf(":"),m=T>0?s.substring(0,T):void 0,F=T>0?s.substring(T+1):void 0;this.nodeRegistry&&(m&&F?d=n({nodeRegistry:this.nodeRegistry,channel:m,chatId:F}):this.dynamicUIToolsFactory&&(d=this.dynamicUIToolsFactory())),this.plasmaClientToolsFactory&&(u=this.plasmaClientToolsFactory(m,F));const p=a(s,this.config.dataDir),f=new o(s,this.config,e,r,t,i,this.nodeToolsFactory?.()??void 0,this.messageToolsFactory?.(s)??void 0,this.serverToolsFactory?.()??void 0,this.cronToolsFactory?.()??void 0,l,c,h,this.ttsToolsFactory?.()??void 0,this.memoryToolsFactory?.()??void 0,this.browserToolsFactory?.()??void 0,g,this.picoToolsFactory?.()??void 0,this.telegramToolsFactory?.()??void 0,this.a2uiToolsFactory?.()??void 0,d??void 0,u??void 0,this.conceptToolsFactory?.()??void 0,p);if(this.channelManager){const s=this.channelManager;if(f.setChannelSender(async(o,t,e,r)=>{r&&r.length>0?await s.sendButtons(o,t,e,[r]):await s.sendToChannel(o,t,e)}),this.showToolUseGetter){const o=this.showToolUseGetter;f.setToolUseNotifier(async(t,e,r)=>{if(!o(`${t}:${e}`))return;const i=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await s.sendToChannel(t,e,i),await s.setTyping(t,e)})}f.setTypingSetter(async(o,t)=>{await s.setTyping(o,t)}),f.setTypingClearer(async(o,t)=>{await s.clearTyping(o,t)}),f.setTextBlockStreamer(async(o,t,e)=>{await s.sendResponse(o,t,e)})}return f.setUsageRecorder((s,o,t,e,r)=>{this.usageBySession.set(s,{totalCostUsd:o,durationMs:t,numTurns:e,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(s,f),f}async interrupt(s){const o=this.agents.get(s);return!!o&&o.interrupt()}isBusy(s){const o=this.agents.get(s);return!!o&&o.isBusy()}hasPendingPermission(s){const o=this.agents.get(s);return!!o&&o.hasPendingPermission()}isFallbackActive(s){const o=this.agents.get(s);return!!o&&o.isFallbackActive()}resolvePermission(s,o){const t=this.agents.get(s);t&&t.resolvePermission(o)}hasPendingQuestion(s){const o=this.agents.get(s);return!!o&&o.hasPendingQuestion()}resolveQuestion(s,o){const t=this.agents.get(s);t&&t.resolveQuestion(o)}destroySession(s){const o=this.agents.get(s);o&&(o.close(),this.agents.delete(s),c.info(`Session agent destroyed: ${s}`))}destroyAll(){for(const[s,o]of this.agents)o.close(),c.info(`Session agent destroyed (reconfigure): ${s}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(s=>{const o=this.agents.get(s);return o&&o.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(s){return this.usageBySession.get(s)}getSdkSlashCommands(){for(const s of this.agents.values()){const o=s.getSdkSlashCommands();if(o.length>0)return o}return[]}async listModels(){try{const o=s({prompt:"list models",options:{maxTurns:0}}),t=await o.supportedModels();for await(const s of o)break;return t.map(s=>({id:s.id??s.name??String(s),name:s.name??s.id??String(s)}))}catch(s){return c.error(`Failed to list models: ${s}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
|
package/dist/config.d.ts
CHANGED
|
@@ -272,6 +272,10 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
272
272
|
agentId: z.ZodDefault<z.ZodString>;
|
|
273
273
|
privateKey: z.ZodDefault<z.ZodString>;
|
|
274
274
|
reconnectDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
275
|
+
rateLimit: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
276
|
+
maxPerPair: z.ZodDefault<z.ZodNumber>;
|
|
277
|
+
windowMs: z.ZodDefault<z.ZodNumber>;
|
|
278
|
+
}, z.core.$strip>>>;
|
|
275
279
|
}, z.core.$strip>>>;
|
|
276
280
|
responses: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
277
281
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -450,6 +454,7 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
450
454
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
451
455
|
isolated: z.ZodDefault<z.ZodBoolean>;
|
|
452
456
|
broadcastEvents: z.ZodDefault<z.ZodBoolean>;
|
|
457
|
+
trackMemory: z.ZodDefault<z.ZodBoolean>;
|
|
453
458
|
storePath: z.ZodDefault<z.ZodString>;
|
|
454
459
|
heartbeat: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
455
460
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -481,6 +486,10 @@ declare const AppConfigSchema: z.ZodObject<{
|
|
|
481
486
|
color: z.ZodDefault<z.ZodString>;
|
|
482
487
|
}, z.core.$strip>>>;
|
|
483
488
|
}, z.core.$strip>>>;
|
|
489
|
+
warmRestart: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
490
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
491
|
+
pm2Name: z.ZodDefault<z.ZodString>;
|
|
492
|
+
}, z.core.$strip>>>;
|
|
484
493
|
}, z.core.$strip>;
|
|
485
494
|
type RawAppConfig = z.infer<typeof AppConfigSchema>;
|
|
486
495
|
export type AppConfig = RawAppConfig & {
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as d}from"dotenv";import{parse as u,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({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()}),j=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),k).default({})}),P=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({enabled:f.boolean().default(!1),brokerUrl:f.string().default("ws://127.0.0.1:3780/ws"),agentId:f.string().default(""),privateKey:f.string().default(""),reconnectDelayMs:f.number().default(5e3)}),S=f.object({telegram:j.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:{}}),mesh:C.optional().default({enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3}),responses:R.optional().default({enabled:!0,port:3e3})}),T=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),A=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),D=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":T.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":A.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")}),U=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),z=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),L=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:U.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:z.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),W=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)}),K={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),model:f.string().default("")}),F={enabled:!0,model:""},q=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:W.optional().default(K),l0:E.optional().default(F)}),G=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),O=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)}),_=[{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}],B=f.array(O).default(_),N=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),X=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(),V={type:"claudecode",piModelRef:""},H=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),Q={enabled:!1,modelRefs:[],rollingMemoryModel:""},Z=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),J={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},Y=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:$.optional().default(V),picoAgent:H.optional().default(Q),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(),G).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(X).default([]),plugins:f.array(N).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Z.optional().default(J),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1})}),ee={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"}},te={enabled:!0,recallStrategy:"builtin-only",dir:"",search:K,l0:F},ae={model:"claude-opus-4-6",mainFallback:"",engine:V,picoAgent:Q,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,apiRetry:J,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},oe=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)}),ne={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},le=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:oe.optional().default(ne)}),re={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:ne},se=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)}),ie={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},de=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:S.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:{}},mesh:{enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3},responses:{enabled:!0,port:3004}}),models:B.optional().default(_),stt:D.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:L.optional().default(ee),memory:q.optional().default(te),agent:Y.optional().default(ae),cron:le.optional().default(re),nostromo:se.optional().default(ie),browser:p.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}})});function ue(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function ce(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(ce);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=ce(o);return t}return e}const fe={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:ee,memory:te,agent:{...ae,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:re,nostromo:ie};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(d({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...fe});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=ce(u(f)),m=de.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=u(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return u(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.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 a=resolveModelEntry(e,t);return a?a.id:t}
|
|
1
|
+
import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as i}from"node:path";import{homedir as s}from"node:os";import{config as d}from"dotenv";import{parse as u,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as m}from"@hera-al/browser-server/config";import{createLogger as p}from"./utils/logger.js";const b=p("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(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({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"]),P=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()}),k=f.object({enabled:f.boolean().default(!1),accounts:f.record(f.string(),P).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({maxPerPair:f.number().default(20),windowMs:f.number().default(36e5)}),S=f.object({enabled:f.boolean().default(!1),brokerUrl:f.string().default("ws://127.0.0.1:3780/ws"),agentId:f.string().default(""),privateKey:f.string().default(""),reconnectDelayMs:f.number().default(5e3),rateLimit:C.optional().default({maxPerPair:20,windowMs:36e5})}),T=f.object({telegram:k.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:{}}),mesh:S.optional().default({enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3,rateLimit:{maxPerPair:20,windowMs:36e5}}),responses:R.optional().default({enabled:!0,port:3e3})}),A=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),D=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),I=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":A.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":D.optional().default({binaryPath:"whisper",model:"base"})}),U=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),L=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),z=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),W=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:U.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:L.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:z.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),K=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)}),E={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},F=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),q={enabled:!0,model:""},G=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:K.optional().default(E),l0:F.optional().default(q)}),N=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),O=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)}),_=[{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}],B=f.array(O).default(_),X=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),$=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)}),V=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),H={type:"claudecode",piModelRef:""},Q=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),Z={enabled:!1,modelRefs:[],rollingMemoryModel:""},J=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),Y={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},ee=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:V.optional().default(H),picoAgent:Q.optional().default(Z),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(),N).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array($).default([]),plugins:f.array(X).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:J.optional().default(Y),skillNudge:f.object({enabled:f.boolean().default(!0),threshold:f.number().default(10)}).default({enabled:!0,threshold:10}),qualityGate:f.object({enabled:f.boolean().default(!1)}).default({enabled:!1})}),te={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"}},ae={enabled:!0,recallStrategy:"builtin-only",dir:"",search:E,l0:q},oe={model:"claude-opus-4-6",mainFallback:"",engine:H,picoAgent:Z,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,apiRetry:Y,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},ne=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)}),le={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},re=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),trackMemory:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:ne.optional().default(le)}),ie={enabled:!0,isolated:!0,broadcastEvents:!1,trackMemory:!1,storePath:"",heartbeat:le},se=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)}),de={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ue=f.object({enabled:f.boolean().default(!1),pm2Name:f.string().default("")}),ce=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:T.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:{}},mesh:{enabled:!1,brokerUrl:"ws://127.0.0.1:3780/ws",agentId:"",privateKey:"",reconnectDelayMs:5e3,rateLimit:{maxPerPair:20,windowMs:36e5}},responses:{enabled:!0,port:3004}}),models:B.optional().default(_),stt:I.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:W.optional().default(te),memory:G.optional().default(ae),agent:ee.optional().default(oe),cron:re.optional().default(ie),nostromo:se.optional().default(de),browser:m.optional().default({enabled:!1,controlPort:3002,headless:!1,noSandbox:!1,attachOnly:!1,remoteCdpTimeoutMs:1500,profiles:{default:{cdpPort:9222,color:"#FF4500"}}}),warmRestart:ue.optional().default({enabled:!1,pm2Name:""})});function fe(e){const t=function(e){const t=new Set,a=/\$\{([^}]+)\}/g;let o;for(;null!==(o=a.exec(e));)t.add(o[1]);return Array.from(t)}(e),a=t.filter(e=>!process.env[e]);a.length>0&&b.warn(`Missing environment variables referenced in config: ${a.join(", ")}. Add them to .env or export them before starting.`)}function me(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(me);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=me(o);return t}return e}const pe={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:te,memory:ae,agent:{...oe,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:ie,nostromo:de};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),s=r(process.cwd(),".env");if(a(s)&&(d({path:s}),b.info(`Loaded .env from ${s}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...pe});t(l,e,"utf-8")}const f=e(l,"utf-8");fe(f);const m=me(u(f)),p=ce.parse(m);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(p.gmabPath),y=i(h,"data"),o(y,{recursive:!0}),!p.timezone){p.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=u(e(l,"utf-8"))??{};a.timezone=p.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${p.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=i(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):i(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:i(y,"core.db"),memoryDir:i(y,"memory"),cronStorePath:n}}(p)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return u(n)??{}}export function resolveModelEntry(e,t){if(!t)return;const a=e.models;if(!a?.length)return;const o=t.indexOf(":");if(o>=0){const e=t.substring(0,o),n=t.substring(o+1);return a.find(t=>t.name===e&&t.id===n)}return a.find(e=>e.name===t)??a.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 a=resolveModelEntry(e,t);return a?a.id:t}
|
package/dist/gateway/bridge.d.ts
CHANGED
|
@@ -29,10 +29,10 @@ export interface ChannelAdapter {
|
|
|
29
29
|
sendText(chatId: string, text: string): Promise<void>;
|
|
30
30
|
sendAudio?(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
|
|
31
31
|
sendButtons?(chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
32
|
+
/** One-shot: fire a single platform-native typing signal. */
|
|
32
33
|
setTyping?(chatId: string): Promise<void>;
|
|
34
|
+
/** One-shot: fire a single platform-native stop-typing signal. */
|
|
33
35
|
clearTyping?(chatId: string): Promise<void>;
|
|
34
|
-
/** Cooperative typing release: decrements refcount instead of force-clearing. */
|
|
35
|
-
releaseTyping?(chatId: string): Promise<void>;
|
|
36
36
|
stop(): Promise<void>;
|
|
37
37
|
}
|
|
38
38
|
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -14,13 +14,6 @@ export declare class ChannelManager {
|
|
|
14
14
|
sendButtons(channelName: string, chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
15
15
|
setTyping(channelName: string, chatId: string): Promise<void>;
|
|
16
16
|
clearTyping(channelName: string, chatId: string): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Cooperative typing release: decrements the inflight refcount instead of
|
|
19
|
-
* force-clearing. The typing interval will self-destruct when no more
|
|
20
|
-
* handlers are in-flight. Use this in the normal message-completion path;
|
|
21
|
-
* reserve clearTyping() as a safety-net for hard resets.
|
|
22
|
-
*/
|
|
23
|
-
releaseTyping(channelName: string, chatId: string): Promise<void>;
|
|
24
17
|
/**
|
|
25
18
|
* Send a full response that may contain MEDIA: lines.
|
|
26
19
|
* Parses out media entries, sends audio via sendAudio, and sends remaining text via sendText.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseMediaLines as t}from"../utils/media-response.js";import{createLogger as e}from"../utils/logger.js";const s=e("ChannelManager");export class ChannelManager{config;tokenDb;onMessage;adapters=new Map;constructor(t,e,s){this.config=t,this.tokenDb=e,this.onMessage=s}registerAdapter(t){this.adapters.set(t.name,t)}async startAll(){const t=[];for(const[e,
|
|
1
|
+
import{parseMediaLines as t}from"../utils/media-response.js";import{createLogger as e}from"../utils/logger.js";const s=e("ChannelManager");export class ChannelManager{config;tokenDb;onMessage;adapters=new Map;constructor(t,e,s){this.config=t,this.tokenDb=e,this.onMessage=s}registerAdapter(t){this.adapters.set(t.name,t)}async startAll(){const t=[];for(const[e,n]of this.adapters)s.info(`Starting channel: ${e}`),t.push(n.start(this.onMessage).then(()=>{s.info(`Channel started: ${e}`)}).catch(t=>{s.error(`Failed to start channel ${e}: ${t}`)}));await Promise.allSettled(t)}async sendToChannel(t,e,n){const a=this.adapters.get(t);a?await a.sendText(e,n):s.error(`Channel not found: ${t}`)}async sendAudio(t,e,n,a){const o=this.adapters.get(t);o?o.sendAudio?await o.sendAudio(e,n,a):s.warn(`Channel ${t} does not support audio, skipping`):s.error(`Channel not found: ${t}`)}async sendButtons(t,e,n,a){const o=this.adapters.get(t);if(o)if(o.sendButtons)await o.sendButtons(e,n,a);else{const t=a.flat().map((t,e)=>`${e+1}. ${t.text}`).join("\n");await o.sendText(e,`${n}\n\n${t}\n\nReply with your choice or type your answer.`)}else s.error(`Channel not found: ${t}`)}async setTyping(t,e){const s=this.adapters.get(t);if(s&&s.setTyping)try{await s.setTyping(e)}catch{}}async clearTyping(t,e){const s=this.adapters.get(t);if(s&&s.clearTyping)try{await s.clearTyping(e)}catch{}}async sendResponse(e,n,a){const{textParts:o,mediaEntries:r}=t(a);for(const t of r)try{await this.sendAudio(e,n,t.path,t.asVoice)}catch(t){s.error(`Failed to send audio to ${e}:${n}: ${t}`)}const i=o.join("\n").trim();i&&await this.sendToChannel(e,n,i)}getAdapter(t){return this.adapters.get(t)}getChannel(t){return this.adapters.get(t)}async sendSystemMessage(t,e,n){const a=this.adapters.get(t);if(a)try{await a.sendText(e,n),s.debug(`System message sent to ${t}:${e}`)}catch(n){s.error(`Failed to send system message to ${t}:${e}: ${n}`)}else s.warn(`Cannot send system message: channel ${t} not found`)}listAdapters(){return[...this.adapters.entries()].map(([t])=>({name:t,active:!0}))}async stopAll(){const t=[];for(const[e,n]of this.adapters)s.info(`Stopping channel: ${e}`),t.push(n.stop().catch(t=>{s.error(`Error stopping channel ${e}: ${t}`)}));await Promise.allSettled(t)}}
|
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
* Dante can reply via sendText("beatrice", "...").
|
|
7
7
|
*/
|
|
8
8
|
import type { ChannelAdapter, MessageHandler } from "../bridge.js";
|
|
9
|
+
export interface MeshRateLimitConfig {
|
|
10
|
+
maxPerPair: number;
|
|
11
|
+
windowMs: number;
|
|
12
|
+
}
|
|
9
13
|
export interface MeshChannelConfig {
|
|
10
14
|
brokerUrl: string;
|
|
11
15
|
agentId: string;
|
|
12
16
|
privateKey: string;
|
|
13
17
|
reconnectDelayMs?: number;
|
|
18
|
+
rateLimit?: MeshRateLimitConfig;
|
|
14
19
|
}
|
|
15
20
|
export declare class MeshChannel implements ChannelAdapter {
|
|
16
21
|
readonly name = "mesh";
|
|
@@ -23,9 +28,25 @@ export declare class MeshChannel implements ChannelAdapter {
|
|
|
23
28
|
private reconnectDelay;
|
|
24
29
|
private stopping;
|
|
25
30
|
private pingTimer;
|
|
26
|
-
/** Track message IDs we sent, so we can
|
|
31
|
+
/** Track message IDs we sent → origin session key, so we can route replies back. */
|
|
27
32
|
private pendingOutbound;
|
|
33
|
+
/** Callback invoked when a reply arrives for an outbound message we sent. */
|
|
34
|
+
private replyCallback;
|
|
35
|
+
/** Track active inbound message per peer — so sendText during processing auto-sets replyTo. */
|
|
36
|
+
private activeInbound;
|
|
37
|
+
private rateBuckets;
|
|
38
|
+
private rateLimitConfig;
|
|
28
39
|
constructor(config: MeshChannelConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Check if sending to a peer is allowed under the rate limit.
|
|
42
|
+
* Returns true if allowed, false if rate-limited.
|
|
43
|
+
*/
|
|
44
|
+
private checkRateLimit;
|
|
45
|
+
/**
|
|
46
|
+
* Register a callback for when a reply arrives for an outbound message.
|
|
47
|
+
* The callback receives the origin session key, the agent ID that replied, and the reply text.
|
|
48
|
+
*/
|
|
49
|
+
onReply(callback: (originSessionKey: string, fromAgent: string, text: string) => void): void;
|
|
29
50
|
start(onMessage: MessageHandler): Promise<void>;
|
|
30
51
|
private connect;
|
|
31
52
|
/**
|
|
@@ -36,7 +57,7 @@ export declare class MeshChannel implements ChannelAdapter {
|
|
|
36
57
|
* If it's a new message (no replyTo matching our outbound), dispatch AND auto-reply.
|
|
37
58
|
*/
|
|
38
59
|
private handleMeshMessage;
|
|
39
|
-
sendText(chatId: string, text: string): Promise<void>;
|
|
60
|
+
sendText(chatId: string, text: string, originSessionKey?: string): Promise<void>;
|
|
40
61
|
/**
|
|
41
62
|
* Send a typed mesh message (for MCP tools).
|
|
42
63
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:crypto";import{WebSocket as t}from"ws";import{createLogger as n}from"../../utils/logger.js";const s=n("Mesh");function
|
|
1
|
+
import e from"node:crypto";import{WebSocket as t}from"ws";import{createLogger as n}from"../../utils/logger.js";const s=n("Mesh");function i(t,n){const s=function(t){const n=Buffer.from(t,"hex"),s=Buffer.from("302e020100300506032b657004220420","hex"),i=Buffer.concat([s,n]);return e.createPrivateKey({key:i,format:"der",type:"pkcs8"})}(n),i=Buffer.from(t,"hex");return e.sign(null,i,s).toString("hex")}export class MeshChannel{name="mesh";ws=null;config;onMessage=null;sessionToken=null;connected=!1;reconnectTimer=null;reconnectDelay;stopping=!1;pingTimer=null;pendingOutbound=new Map;replyCallback=null;activeInbound=new Map;rateBuckets=new Map;rateLimitConfig;constructor(e){this.config=e,this.reconnectDelay=e.reconnectDelayMs??5e3,this.rateLimitConfig={maxPerPair:e.rateLimit?.maxPerPair??20,windowMs:e.rateLimit?.windowMs??36e5}}checkRateLimit(e){const t=Date.now(),n=this.rateBuckets.get(e);if(!n||t-n.windowStart>=this.rateLimitConfig.windowMs)return this.rateBuckets.set(e,{count:1,windowStart:t}),!0;if(n.count>=this.rateLimitConfig.maxPerPair){const i=this.rateLimitConfig.windowMs-(t-n.windowStart);return s.warn(`Rate limit reached for peer ${e}: ${n.count}/${this.rateLimitConfig.maxPerPair} in window. Resets in ${Math.ceil(i/1e3)}s`),!1}return n.count++,!0}onReply(e){this.replyCallback=e}async start(e){this.onMessage=e,this.stopping=!1,await this.connect()}connect(){return new Promise((e,n)=>{const{brokerUrl:o,agentId:r}=this.config;s.info(`Connecting to mesh broker at ${o} as ${r}...`);try{this.ws=new t(o)}catch(t){return s.error(`Failed to create WebSocket: ${t}`),this.scheduleReconnect(),void e()}let a=!1;const c=t=>{a||(a=!0,t?(s.error(`Mesh connection failed: ${t.message}`),this.scheduleReconnect(),e()):e())},h=setTimeout(()=>{c(new Error("Connection timeout")),this.ws?.close()},1e4);this.ws.on("open",()=>{}),this.ws.on("message",e=>{let t;try{t=JSON.parse(e.toString())}catch{return void s.warn("Invalid JSON from mesh broker")}switch(t.type){case"challenge":{const e=i(t.challenge,this.config.privateKey);this.sendRaw({type:"auth",agentId:this.config.agentId,signature:e,timestamp:Date.now()});break}case"auth_result":clearTimeout(h),t.authenticated&&t.sessionToken?(this.connected=!0,this.sessionToken=t.sessionToken,s.info(`Authenticated as ${this.config.agentId} on mesh`),this.startPing(),c()):c(new Error(`Auth failed: ${t.error??"unknown"}`));break;case"message":this.handleMeshMessage(t.message),this.sendRaw({type:"ack",messageId:t.message.id});break;case"presence":s.info(`Mesh presence: ${t.agentId} is ${t.status}`),this.knownPeers.set(t.agentId,t.status);break;case"ping":this.sendRaw({type:"pong"});break;case"pong":case"ack":break;case"error":s.warn(`Mesh error: ${t.error}`)}}),this.ws.on("close",(e,t)=>{clearTimeout(h),this.connected=!1,this.sessionToken=null,this.stopPing(),s.info(`Mesh connection closed (code=${e}, reason=${t?.toString()??"?"})`),this.stopping||this.scheduleReconnect(),c()}),this.ws.on("error",e=>{s.error(`Mesh WebSocket error: ${e.message}`)})})}async handleMeshMessage(t){if(!this.onMessage)return;const n=t.from,i=null!=t.replyTo&&this.pendingOutbound.has(t.replyTo),o=i&&t.replyTo?this.pendingOutbound.get(t.replyTo):void 0;if(t.replyTo&&this.pendingOutbound.has(t.replyTo)){const e=t.replyTo;setTimeout(()=>this.pendingOutbound.delete(e),3e5)}let r;if("string"==typeof t.payload)r=t.payload;else if(t.payload&&"object"==typeof t.payload){const e=t.payload;r="string"==typeof e.text?e.text:`[Mesh ${t.type}] ${JSON.stringify(t.payload)}`}else r=`[Mesh ${t.type}]`;if(i){if(s.info(`Reply from ${t.from} (to outbound ${t.replyTo}) → routing to origin session${o?` [${o}]`:""}`),o&&this.replyCallback)return void this.replyCallback(o,t.from,r)}else s.info(`New message from ${t.from}: ${t.type} → dispatching to agent (will auto-reply)`);const a={chatId:n,userId:t.from,channelName:"mesh",text:r,attachments:[],username:t.from};this.activeInbound.set(t.from,t.id);try{const o=await this.onMessage(a);if(!i&&o&&o.trim())if(this.checkRateLimit(n)){const i={id:e.randomUUID(),from:this.config.agentId,to:n,type:"text",payload:{text:o},timestamp:Date.now(),replyTo:t.id};this.sendRaw({type:"message",message:i}),s.debug(`Auto-replied to ${t.from}: ${o.slice(0,80)}...`)}else s.warn(`Dropping auto-reply to ${n}: rate limited`)}catch(e){s.error(`Error handling mesh message from ${t.from}: ${e}`)}finally{this.activeInbound.delete(t.from)}}async sendText(t,n,i){if(!this.connected||!this.ws)return void s.warn(`Cannot send to ${t}: mesh not connected`);if(!this.checkRateLimit(t))return void s.warn(`Dropping outbound message to ${t}: rate limited`);const o=e.randomUUID(),r=this.activeInbound.get(t),a={id:o,from:this.config.agentId,to:t,type:"text",payload:{text:n},timestamp:Date.now(),...r?{replyTo:r}:{}};r||this.pendingOutbound.set(o,i),this.sendRaw({type:"message",message:a}),s.debug(`Sent to ${t} (id=${o}${r?`, replyTo=${r}`:""}${i?`, origin=${i}`:""}): ${n.slice(0,80)}...`)}sendTyped(t,n,s,i){if(!this.connected||!this.ws)throw new Error("Mesh not connected");if(!this.checkRateLimit(t))throw new Error(`Rate limit reached for peer ${t} (max ${this.rateLimitConfig.maxPerPair} per ${this.rateLimitConfig.windowMs/1e3}s)`);const o={id:e.randomUUID(),from:this.config.agentId,to:t,type:n,payload:s,timestamp:Date.now(),replyTo:i};return this.sendRaw({type:"message",message:o}),o.id}getSessionToken(){return this.sessionToken}getAgentId(){return this.config.agentId}getBrokerHttpUrl(){return this.config.brokerUrl.replace("ws://","http://").replace("wss://","https://").replace("/ws","")}isConnected(){return this.connected}knownPeers=new Map;async getAgents(){try{const e=this.getBrokerHttpUrl(),t=await fetch(`${e}/api/agents`,{headers:this.sessionToken?{Authorization:`Bearer ${this.sessionToken}`}:{}});if(!t.ok)throw new Error(`HTTP ${t.status}`);const n=await t.json();return Array.isArray(n.agents)?n.agents:[]}catch(e){s.warn(`Failed to fetch agents: ${e}`);const t=[];for(const[e,n]of this.knownPeers)t.push({agentId:e,status:n});return t}}async getMeshPromptInfo(){const e=(await this.getAgents()).filter(e=>e.agentId!==this.config.agentId);if(0===e.length)return"";const t=e.map(e=>`- **${e.agentId}** (${e.status??"unknown"})`);return["## Mesh (Inter-Agent Communication)",`You are connected to the Hera Mesh as **${this.config.agentId}**.`,"Other agents on the mesh:",...t,"",'To message an agent: use send_message(channel="mesh", chatId="<agentId>", text="...").',"When you receive a mesh message, it appears as a normal incoming message from channel=mesh."].join("\n")}sendRaw(e){this.ws&&this.ws.readyState===t.OPEN&&this.ws.send(JSON.stringify(e))}startPing(){this.stopPing(),this.pingTimer=setInterval(()=>{this.connected&&this.sendRaw({type:"ping"})},3e4)}stopPing(){this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null)}scheduleReconnect(){this.stopping||this.reconnectTimer||(s.info(`Reconnecting in ${this.reconnectDelay}ms...`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect().catch(e=>{s.error(`Reconnect failed: ${e}`)})},this.reconnectDelay))}async stop(){this.stopping=!0,this.stopPing(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(1e3,"Channel stopping"),this.ws=null),this.connected=!1,s.info("Mesh channel stopped")}}
|
|
@@ -19,19 +19,13 @@ export declare class TelegramChannel implements ChannelAdapter {
|
|
|
19
19
|
private config;
|
|
20
20
|
private accountId;
|
|
21
21
|
private tokenDb;
|
|
22
|
-
|
|
23
|
-
private inflightTyping;
|
|
24
|
-
private inflightCount;
|
|
25
|
-
constructor(config: TelegramAccountConfig, accountId: string, tokenDb: TokenDB, inflightTyping?: boolean);
|
|
22
|
+
constructor(config: TelegramAccountConfig, accountId: string, tokenDb: TokenDB);
|
|
26
23
|
start(onMessage: MessageHandler): Promise<void>;
|
|
27
24
|
sendText(chatId: string, text: string): Promise<void>;
|
|
25
|
+
/** One-shot: send a single "typing" signal to Telegram. */
|
|
28
26
|
setTyping(chatId: string): Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
clearTyping(chatId: string): Promise<void>;
|
|
32
|
-
releaseTyping(chatId: string): Promise<void>;
|
|
33
|
-
private startTypingInterval;
|
|
34
|
-
private stopTypingInterval;
|
|
27
|
+
/** One-shot: Telegram has no explicit "stop typing" API — it clears on message send. */
|
|
28
|
+
clearTyping(_chatId: string): Promise<void>;
|
|
35
29
|
sendButtons(chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
36
30
|
sendAudio(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
|
|
37
31
|
reactMessage(chatId: string, messageId: string, emoji: string, remove?: boolean): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Bot as
|
|
1
|
+
import{Bot as e,InputFile as t}from"grammy";import{validateChannelUser as i}from"../../../auth/auth-middleware.js";import{parseMediaLines as o}from"../../../utils/media-response.js";import{markdownToTelegramHtmlChunks as a}from"../../../utils/telegram-format.js";import{createLogger as n}from"../../../utils/logger.js";import{sendMessageTelegram as r}from"./send.js";import{reactMessageTelegram as s,resolveReactionLevel as c}from"./reactions.js";import{editMessageTelegram as d,deleteMessageTelegram as l}from"./edit-delete.js";import{sendStickerTelegram as m,cacheSticker as f}from"./stickers.js";import{validateButtonsForChatId as u}from"./inline-buttons.js";const h=n("Telegram");export class TelegramChannel{name="telegram";bot;config;accountId;tokenDb;constructor(t,i,o){this.config=t,this.accountId=i,this.tokenDb=o,this.bot=new e(t.botToken)}async start(e){this.bot.on("message",t=>{this.handleIncoming(t,e)}),this.bot.on("callback_query:data",t=>{const i=t.callbackQuery.data,o=String(t.from?.id??"unknown"),a=String(t.chat?.id??t.callbackQuery.message?.chat?.id??"unknown"),n=t.from?.username;t.answerCallbackQuery().catch(()=>{});e({chatId:a,userId:o,channelName:"telegram",text:i,attachments:[],username:n,rawContext:t}).then(async e=>{e&&e.trim()&&await this.sendText(a,e)}).catch(e=>{h.error(`Error handling callback from ${o}: ${e}`)})}),h.info("Starting Telegram bot..."),this.bot.start({onStart:e=>{h.info(`Telegram bot started: @${e.username}`)}}).catch(e=>{String(e).includes("Aborted delay")?h.debug("Telegram polling stopped"):h.error(`Telegram bot polling error: ${e}`)})}async sendText(e,t){try{await r(e,t,{token:this.config.botToken,accountId:this.accountId,textChunkLimit:this.config.textChunkLimit,linkPreview:this.config.linkPreview,retry:this.config.retry})}catch(e){throw h.error(`Failed to send message: ${e}`),e}}async setTyping(e){await this.bot.api.sendChatAction(e,"typing").catch(()=>{})}async clearTyping(e){}async sendButtons(e,t,i){const o=i.map(e=>e.map(e=>({text:e.text,callback_data:e.callbackData??e.text})));try{u(o,this.config,e)}catch(i){return h.warn(`Button validation failed: ${i}, sending without buttons`),void await this.sendText(e,t)}await r(e,t,{token:this.config.botToken,accountId:this.accountId,buttons:o,textChunkLimit:this.config.textChunkLimit,linkPreview:this.config.linkPreview,retry:this.config.retry})}async sendAudio(e,i,o){const a=new t(i);o?await this.bot.api.sendVoice(e,a):await this.bot.api.sendAudio(e,a)}async reactMessage(e,t,i,o){const{level:a}=c(this.config);"off"!==a?await s(e,t,i,this.config.botToken,{remove:o}):h.debug("Reactions disabled for this account")}async editMessage(e,t,i,o){const a=o?.map(e=>({text:e.text,callback_data:e.callbackData??e.text}));await d(e,t,i,this.config.botToken,{buttons:a?[a]:void 0,linkPreview:this.config.linkPreview})}async deleteMessage(e,t){await l(e,t,this.config.botToken,this.config.retry)}async sendSticker(e,t){await m(e,t,this.config.botToken,{retry:this.config.retry})}async stop(){try{await this.bot.stop(),h.info("Telegram bot stopped"),await new Promise(e=>setTimeout(e,100))}catch(e){h.warn(`Error stopping Telegram bot: ${e}`)}}async handleIncoming(e,a){const n=String(e.from?.id??"unknown"),r=String(e.chat?.id??"unknown"),s=e.from?.username,c=i(this.tokenDb,n,"telegram",this.config.dmPolicy,this.config.allowFrom);if(!c.authorized)return h.warn(`Unauthorized message from ${n} (@${s})`),void await e.reply(c.reason??"Not authorized.");try{const i=await this.buildIncomingMessage(e,r,n,s),c=await a(i),{textParts:d,mediaEntries:l}=o(c);for(const i of l)try{const o=new t(i.path);i.asVoice?await e.replyWithVoice(o):await e.replyWithAudio(o)}catch(e){h.error(`Failed to send audio: ${e}`)}const m=d.join("\n").trim();m&&await this.sendChunked(e,m)}catch(t){h.error(`Error handling message from ${n}: ${t}`),await e.reply("An error occurred while processing your message.").catch(()=>{})}}async buildIncomingMessage(e,t,i,o){const a=e.message,n=[];let r=a.text??a.caption??void 0;if(a.photo&&a.photo.length>0){const t=a.photo[a.photo.length-1];n.push({type:"image",mimeType:"image/jpeg",fileSize:t.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(e,t.file_id)})}if(a.voice&&n.push({type:"voice",mimeType:a.voice.mime_type??"audio/ogg",duration:a.voice.duration,fileSize:a.voice.file_size,getBuffer:()=>this.downloadFile(e,a.voice.file_id)}),a.audio&&n.push({type:"audio",mimeType:a.audio.mime_type??"audio/mpeg",fileName:a.audio.file_name,duration:a.audio.duration,fileSize:a.audio.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(e,a.audio.file_id)}),a.document&&n.push({type:"document",mimeType:a.document.mime_type,fileName:a.document.file_name,fileSize:a.document.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(e,a.document.file_id)}),a.video&&n.push({type:"video",mimeType:a.video.mime_type??"video/mp4",fileName:a.video.file_name,duration:a.video.duration,fileSize:a.video.file_size,caption:a.caption,getBuffer:()=>this.downloadFile(e,a.video.file_id)}),a.video_note&&n.push({type:"video_note",mimeType:"video/mp4",duration:a.video_note.duration,fileSize:a.video_note.file_size,getBuffer:()=>this.downloadFile(e,a.video_note.file_id)}),a.sticker){if(this.config.actions?.sticker)try{f({fileId:a.sticker.file_id,fileUniqueId:a.sticker.file_unique_id,emoji:a.sticker.emoji,setName:a.sticker.set_name,description:a.sticker.emoji?`${a.sticker.emoji} sticker${a.sticker.set_name?` from ${a.sticker.set_name}`:""}`:a.sticker.set_name??"sticker"})}catch(e){h.warn(`Failed to cache sticker: ${e}`)}n.push({type:"sticker",mimeType:a.sticker.is_animated?"application/x-tgsticker":a.sticker.is_video?"video/webm":"image/webp",metadata:{emoji:a.sticker.emoji,setName:a.sticker.set_name},getBuffer:()=>this.downloadFile(e,a.sticker.file_id)})}return a.location&&n.push({type:"location",metadata:{latitude:a.location.latitude,longitude:a.location.longitude},getBuffer:async()=>Buffer.alloc(0)}),a.contact&&n.push({type:"contact",metadata:{phoneNumber:a.contact.phone_number,firstName:a.contact.first_name,lastName:a.contact.last_name,userId:a.contact.user_id},getBuffer:async()=>Buffer.alloc(0)}),{chatId:t,userId:i,channelName:"telegram",text:r,attachments:n,username:o,rawContext:e}}async downloadFile(e,t){const i=await e.api.getFile(t),o=`https://api.telegram.org/file/bot${this.config.botToken}/${i.file_path}`,a=await fetch(o);if(!a.ok)throw new Error(`Failed to download file: ${a.statusText}`);return Buffer.from(await a.arrayBuffer())}async sendChunked(e,t){const i=a(t,4096);for(const o of i)try{await e.reply(o,{parse_mode:"HTML"})}catch{const i=g(t,4096);for(const t of i)await e.reply(t);break}}}function g(e,t){if(e.length<=t)return[e];const i=[];let o=e;for(;o.length>0;){if(o.length<=t){i.push(o);break}let e=o.lastIndexOf("\n",t);e<=0&&(e=t),i.push(o.slice(0,e)),o=o.slice(e).trimStart()}return i}
|
|
@@ -9,8 +9,6 @@ export declare class WebChatChannel implements ChannelAdapter {
|
|
|
9
9
|
readonly name = "webchat";
|
|
10
10
|
private connections;
|
|
11
11
|
private onMessage;
|
|
12
|
-
private typingIntervals;
|
|
13
|
-
private inflightCount;
|
|
14
12
|
private pendingMessages;
|
|
15
13
|
start(onMessage: MessageHandler): Promise<void>;
|
|
16
14
|
registerConnection(chatId: string, ws: WebSocket): void;
|
|
@@ -23,6 +21,7 @@ export declare class WebChatChannel implements ChannelAdapter {
|
|
|
23
21
|
/**
|
|
24
22
|
* Handle a chat message arriving from a paired node.
|
|
25
23
|
* Fire-and-forget: caller does not await.
|
|
24
|
+
* Typing lifecycle is fully managed by TypingCoordinator via server.ts.
|
|
26
25
|
*/
|
|
27
26
|
handleNodeChat(chatId: string, nodeId: string, msg: {
|
|
28
27
|
text?: string;
|
|
@@ -30,14 +29,12 @@ export declare class WebChatChannel implements ChannelAdapter {
|
|
|
30
29
|
}): Promise<void>;
|
|
31
30
|
sendText(chatId: string, text: string): Promise<void>;
|
|
32
31
|
sendButtons(chatId: string, text: string, buttons: InlineButton[][]): Promise<void>;
|
|
32
|
+
/** One-shot: send a single typing=true signal to the webchat client. */
|
|
33
33
|
setTyping(chatId: string): Promise<void>;
|
|
34
|
+
/** One-shot: send typing=false to the webchat client. */
|
|
34
35
|
clearTyping(chatId: string): Promise<void>;
|
|
35
|
-
releaseTyping(chatId: string): Promise<void>;
|
|
36
36
|
sendAudio(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
|
|
37
37
|
stop(): Promise<void>;
|
|
38
|
-
private startTypingInterval;
|
|
39
|
-
private stopTypingInterval;
|
|
40
|
-
private resendTypingIfActive;
|
|
41
38
|
private sendWs;
|
|
42
39
|
private enqueuePending;
|
|
43
40
|
private flushPending;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as
|
|
1
|
+
import{readFileSync as e}from"node:fs";import{basename as t,extname as s}from"node:path";import{parseMediaLines as n}from"../../utils/media-response.js";import{createLogger as a}from"../../utils/logger.js";const o=a("WebChat");export function buildWebChatId(e,t){return`${(e||"node").toLowerCase().replace(/[^a-z0-9_-]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"")||"node"}-${t.slice(0,8)}`}export class WebChatChannel{name="webchat";connections=new Map;onMessage=null;pendingMessages=new Map;async start(e){this.onMessage=e,o.info("WebChat channel started (virtual — connections managed by Nostromo)")}registerConnection(e,t){const s=!this.connections.has(e);this.connections.set(e,t),s&&o.info(`WebChat connection registered: ${e}`),this.flushPending(e)}unregisterConnection(e){this.connections.delete(e)&&o.info(`WebChat connection unregistered: ${e}`)}unregisterByWs(e){for(const[t,s]of this.connections)s===e&&(this.connections.delete(t),o.info(`WebChat connection unregistered: ${t}`))}async handleNodeChat(e,t,s){if(!this.onMessage)return void o.warn("WebChat: message received but no onMessage handler registered");const a=[];if(s.attachments&&Array.isArray(s.attachments))for(const e of s.attachments)a.push({type:e.type,mimeType:e.mimeType,fileName:e.fileName,duration:e.duration,caption:e.caption,getBuffer:()=>Promise.resolve(Buffer.from(e.data,"base64"))});const i={chatId:e,userId:t,channelName:"webchat",text:s.text,attachments:a,username:e};try{const t=await this.onMessage(i),{textParts:s,mediaEntries:a}=n(t);for(const t of a)try{await this.sendAudio(e,t.path,t.asVoice)}catch(t){o.error(`WebChat: failed to send audio to ${e}: ${t}`)}const r=s.join("\n").trim();r&&this.sendWs(e,{type:"chat_response",role:"assistant",text:r})}catch(t){o.error(`WebChat: error handling message from ${e}: ${t}`),this.sendWs(e,{type:"chat_response",role:"assistant",text:"Error processing message."})}}async sendText(e,t){this.sendWs(e,{type:"chat_message",role:"assistant",text:t})}async sendButtons(e,t,s){const n=s.flat();this.sendWs(e,{type:"chat_message",role:"assistant",text:t,buttons:n.map(e=>({text:e.text,callbackData:e.callbackData??e.text,...e.url?{url:e.url}:{}}))})}async setTyping(e){this.sendWs(e,{type:"typing_indicator",typing:!0},!1)}async clearTyping(e){this.sendWs(e,{type:"typing_indicator",typing:!1},!1)}async sendAudio(n,a,i){try{const o=e(a),r=t(a),c=s(a).toLowerCase().replace(".",""),d={mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/ogg",wav:"audio/wav",m4a:"audio/mp4",flac:"audio/flac"}[c]||"audio/mpeg";this.sendWs(n,{type:"chat_media",role:"assistant",mediaType:"audio",mimeType:d,fileName:r,data:o.toString("base64"),asVoice:i??!1})}catch(e){o.error(`WebChat: failed to read audio file ${a}: ${e}`)}}async stop(){this.onMessage=null,o.info("WebChat channel stopped")}sendWs(e,t,s=!0){const n=this.connections.get(e);if(n&&n.readyState===n.OPEN)try{n.send(JSON.stringify({...t,chatId:e}))}catch(t){o.error(`WebChat: failed to send to ${e}: ${t}`)}else s&&this.enqueuePending(e,t)}enqueuePending(e,t){let s=this.pendingMessages.get(e);for(s||(s=[],this.pendingMessages.set(e,s)),s.push(t);s.length>10;)s.shift();o.info(`WebChat: queued pending message for ${e} (${s.length}/10)`)}flushPending(e){const t=this.pendingMessages.get(e);if(t&&0!==t.length){o.info(`WebChat: flushing ${t.length} pending message(s) for ${e}`),this.pendingMessages.delete(e);for(const s of t)this.sendWs(e,s,!1)}}}
|
|
@@ -12,10 +12,7 @@ export declare class WhatsAppChannel implements ChannelAdapter {
|
|
|
12
12
|
private qrCallback;
|
|
13
13
|
private connected;
|
|
14
14
|
private stopping;
|
|
15
|
-
|
|
16
|
-
private inflightTyping;
|
|
17
|
-
private inflightCount;
|
|
18
|
-
constructor(config: WhatsAppAccountConfig, inflightTyping?: boolean);
|
|
15
|
+
constructor(config: WhatsAppAccountConfig);
|
|
19
16
|
setQrCallback(cb: QrCallback): void;
|
|
20
17
|
isConnected(): boolean;
|
|
21
18
|
start(onMessage: MessageHandler): Promise<void>;
|
|
@@ -27,13 +24,10 @@ export declare class WhatsAppChannel implements ChannelAdapter {
|
|
|
27
24
|
private handleIncoming;
|
|
28
25
|
private checkAccess;
|
|
29
26
|
sendText(chatId: string, text: string): Promise<void>;
|
|
27
|
+
/** One-shot: send a single "composing" signal to WhatsApp. */
|
|
30
28
|
setTyping(chatId: string): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
private resendTypingIfActive;
|
|
29
|
+
/** One-shot: send "paused" to WhatsApp to stop the typing indicator. */
|
|
33
30
|
clearTyping(chatId: string): Promise<void>;
|
|
34
|
-
releaseTyping(chatId: string): Promise<void>;
|
|
35
|
-
private startTypingInterval;
|
|
36
|
-
private stopTypingInterval;
|
|
37
31
|
sendAudio(chatId: string, filePath: string, asVoice?: boolean): Promise<void>;
|
|
38
32
|
stop(): Promise<void>;
|
|
39
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DisconnectReason as
|
|
1
|
+
import{DisconnectReason as e,fetchLatestBaileysVersion as s,makeCacheableSignalKeyStore as t,makeWASocket as o,useMultiFileAuthState as n}from"@whiskeysockets/baileys";import{mkdirSync as c,existsSync as i,readFileSync as a}from"node:fs";import{resolve as r}from"node:path";import{renderQrPngBase64 as h}from"./qr-image.js";import{convertMarkdownTables as d}from"../../utils/markdown/tables.js";import{chunkText as l}from"../../utils/chunk.js";import{createLogger as p}from"../../utils/logger.js";const u=p("WhatsApp");export class WhatsAppChannel{name="whatsapp";sock=null;config;qrCallback=null;connected=!1;stopping=!1;constructor(e){this.config=e}setQrCallback(e){this.qrCallback=e}isConnected(){return this.connected}async start(e){const s=r(this.config.authDir);i(s)||c(s,{recursive:!0}),await this.connect(s,e)}async connect(c,i){const{state:a,saveCreds:r}=await n(c),{version:d}=await s(),l={level:"silent",trace:()=>{},debug:()=>{},info:()=>{},warn:()=>{},error:()=>{},fatal:()=>{},child:()=>l};this.sock=o({auth:{creds:a.creds,keys:t(a.keys,l)},version:d,logger:l,printQRInTerminal:!1,browser:["GrabMeABeer","Web","1.0"],syncFullHistory:!1,markOnlineOnConnect:!1}),this.sock.ev.on("creds.update",r),this.sock.ev.on("connection.update",async s=>{try{const{connection:t,lastDisconnect:o,qr:n}=s;if(n){u.info("QR code received, rendering...");const e=`data:image/png;base64,${await h(n)}`;this.qrCallback?.(e,!1)}if("open"===t&&(this.connected=!0,u.info("WhatsApp connected"),this.qrCallback?.(null,!0)),"close"===t){this.connected=!1;const s=o?.error?.output?.statusCode??o?.error?.status;s===e.loggedOut?(u.warn("WhatsApp session logged out. Re-scan QR via Nostromo."),this.qrCallback?.(null,!1,"Session logged out. Please re-scan QR code.")):this.stopping||(u.info(`WhatsApp disconnected (code ${s}), reconnecting...`),setTimeout(()=>this.connect(c,i),3e3))}}catch(e){u.error(`connection.update handler error: ${e}`)}}),this.sock.ws&&"function"==typeof this.sock.ws.on&&this.sock.ws.on("error",e=>{u.error(`WebSocket error: ${e.message}`)}),this.sock.ev.on("messages.upsert",({messages:e,type:s})=>{if("notify"===s)for(const s of e){if(!s.message||s.key.fromMe)continue;const e=s.key.remoteJid;if(!e)continue;const t=e.replace(/@s\.whatsapp\.net$/,""),o=s.message.conversation??s.message.extendedTextMessage?.text??void 0;if(!this.checkAccess(t)){u.warn(`Unauthorized message from ${t}`),this.sock?.sendMessage(e,{text:"Not authorized."});continue}if(!o)continue;const n={chatId:e,userId:t,channelName:"whatsapp",text:o,attachments:[],username:s.pushName??void 0};this.handleIncoming(n,e,t,i)}})}async handleIncoming(e,s,t,o){try{const t=await o(e),n=d(t,"code"),c=l(n,4e3);for(const e of c)await(this.sock?.sendMessage(s,{text:e}))}catch(e){u.error(`Error handling message from ${t}: ${e}`)}}checkAccess(e){const s=this.config.dmPolicy||"allowlist";if("open"===s)return!0;if("allowlist"===s){return(this.config.allowFrom??[]).some(s=>String(s)===e)}return!0}async sendText(e,s){if(!this.sock)return;const t=d(s,"code"),o=l(t,4e3);for(const s of o)await this.sock.sendMessage(e,{text:s})}async setTyping(e){this.sock&&await this.sock.sendPresenceUpdate("composing",e).catch(()=>{})}async clearTyping(e){this.sock?.sendPresenceUpdate("paused",e).catch(()=>{})}async sendAudio(e,s,t){if(!this.sock)return;const o=a(s);t?await this.sock.sendMessage(e,{audio:o,mimetype:"audio/ogg; codecs=opus",ptt:!0}):await this.sock.sendMessage(e,{audio:o,mimetype:"audio/mpeg"})}async stop(){this.stopping=!0;try{this.sock?.ws?.close()}catch{}this.sock=null,this.connected=!1,u.info("WhatsApp stopped")}}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ChannelManager } from "./channel-manager.js";
|
|
2
|
+
/**
|
|
3
|
+
* Centralised typing-indicator lifecycle manager.
|
|
4
|
+
*
|
|
5
|
+
* Owns the refcount and the renewal interval for every (channel, chatId) pair.
|
|
6
|
+
* Channel adapters are reduced to dumb one-shot signal senders:
|
|
7
|
+
* - setTyping() → fire a single platform-native "typing" signal
|
|
8
|
+
* - clearTyping() → fire a single platform-native "stop typing" signal
|
|
9
|
+
*
|
|
10
|
+
* The coordinator calls these at the right time; adapters no longer manage
|
|
11
|
+
* intervals, refcounts, or any typing state of their own.
|
|
12
|
+
*/
|
|
13
|
+
export declare class TypingCoordinator {
|
|
14
|
+
private channelManager;
|
|
15
|
+
/** How many handlers are actively processing for this key. */
|
|
16
|
+
private refcounts;
|
|
17
|
+
/** Self-renewing intervals that pump typing signals. */
|
|
18
|
+
private intervals;
|
|
19
|
+
constructor(channelManager: ChannelManager);
|
|
20
|
+
private key;
|
|
21
|
+
/**
|
|
22
|
+
* A handler started processing a message — increment refcount and ensure
|
|
23
|
+
* a typing-renewal loop is running.
|
|
24
|
+
*/
|
|
25
|
+
acquire(channel: string, chatId: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* A handler finished processing — decrement refcount.
|
|
28
|
+
* The loop will self-destruct at the next tick when it sees count === 0.
|
|
29
|
+
*/
|
|
30
|
+
release(channel: string, chatId: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Reinforce typing after a message send (Telegram cancels typing on send).
|
|
33
|
+
* Only sends if there are still active handlers.
|
|
34
|
+
*/
|
|
35
|
+
reinforceIfActive(channel: string, chatId: string): void;
|
|
36
|
+
/** Force-clear everything — safety net for hard resets / shutdown. */
|
|
37
|
+
forceStop(channel: string, chatId: string): void;
|
|
38
|
+
/** Clear all intervals (called on server shutdown). */
|
|
39
|
+
stopAll(): void;
|
|
40
|
+
private startLoop;
|
|
41
|
+
private stopLoop;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=typing-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createLogger as t}from"../utils/logger.js";t("TypingCoordinator");export class TypingCoordinator{channelManager;refcounts=new Map;intervals=new Map;constructor(t){this.channelManager=t}key(t,e){return`${t}:${e}`}acquire(t,e){const s=this.key(t,e),n=(this.refcounts.get(s)??0)+1;this.refcounts.set(s,n),1===n&&this.startLoop(t,e)}release(t,e){const s=this.key(t,e),n=Math.max(0,(this.refcounts.get(s)??0)-1);n>0?this.refcounts.set(s,n):(this.refcounts.delete(s),this.stopLoop(t,e))}reinforceIfActive(t,e){const s=this.key(t,e);(this.refcounts.get(s)??0)>0&&this.channelManager.setTyping(t,e).catch(()=>{})}forceStop(t,e){const s=this.key(t,e);this.refcounts.delete(s),this.stopLoop(t,e)}stopAll(){for(const[,t]of this.intervals)clearInterval(t);this.intervals.clear(),this.refcounts.clear()}startLoop(t,e){const s=this.key(t,e),n=this.intervals.get(s);n&&clearInterval(n),this.channelManager.setTyping(t,e).catch(()=>{});const r=setInterval(()=>{(this.refcounts.get(s)??0)>0?this.channelManager.setTyping(t,e).catch(()=>{}):this.stopLoop(t,e)},4e3);this.intervals.set(s,r)}stopLoop(t,e){const s=this.key(t,e),n=this.intervals.get(s);n&&(clearInterval(n),this.intervals.delete(s)),this.channelManager.clearTyping(t,e).catch(()=>{})}}
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as a}from"./auth/token-db.js";import{NodeSignatureDB as r}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{MeshChannel as d}from"./gateway/channels/mesh.js";import{ResponsesChannel as f}from"./channels/responses.js";import{AgentService as p}from"./agent/agent-service.js";import{SessionManager as u}from"./agent/session-manager.js";import{buildPrompt as b,buildSystemPrompt as y}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as S,loadWorkspaceFiles as w}from"./agent/workspace-files.js";import{NodeRegistry as v}from"./gateway/node-registry.js";import{MemoryManager as M}from"./memory/memory-manager.js";import{MessageProcessor as R}from"./media/message-processor.js";import{loadSTTProvider as C}from"./stt/stt-loader.js";import{CommandRegistry as A}from"./commands/command-registry.js";import{NewCommand as T}from"./commands/new.js";import{CompactCommand as k}from"./commands/compact.js";import{ModelCommand as $,DefaultModelCommand as j}from"./commands/model.js";import{StopCommand as D}from"./commands/stop.js";import{HelpCommand as I}from"./commands/help.js";import{McpCommand as x}from"./commands/mcp.js";import{ModelsCommand as E}from"./commands/models.js";import{CoderCommand as _}from"./commands/coder.js";import{SandboxCommand as P}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as U}from"./commands/customsubagents.js";import{StatusCommand as F}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as O}from"./commands/usage.js";import{DebugA2UICommand as H}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as L}from"./cron/cron-service.js";import{stripHeartbeatToken as Q,isHeartbeatContentEffectivelyEmpty as W}from"./cron/heartbeat-token.js";import{createServerToolsServer as z}from"./tools/server-tools.js";import{createCronToolsServer as G}from"./tools/cron-tools.js";import{createTTSToolsServer as q}from"./tools/tts-tools.js";import{createMemoryToolsServer as V}from"./tools/memory-tools.js";import{createBrowserToolsServer as J}from"./tools/browser-tools.js";import{createPicoToolsServer as X}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as Y}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Z}from"./tools/concept-tools.js";import{BrowserService as ee}from"./browser/browser-service.js";import{MemorySearch as te}from"./memory/memory-search.js";import{ConceptStore as se}from"./memory/concept-store.js";import{stripMediaLines as ne}from"./utils/media-response.js";import{loadConfig as oe,loadRawConfig as ie,backupConfig as ae,resolveModelEntry as re,modelRefName as he}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as ge}from"./utils/logger.js";import{SessionErrorHandler as le}from"./agent/session-error-handler.js";import{initStickerCache as me}from"./gateway/channels/telegram/stickers.js";const de=ge("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;meshChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new a(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new r(e.dbPath),this.nodeRegistry=new v,this.sessionManager=new u(this.sessionDb),e.memory.enabled&&(this.memoryManager=new M(e.memoryDir,e.timezone));const t=C(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,n),me(e.dataDir),this.commandRegistry=new A,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>z(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new ee,this.conceptStore=new se(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new p(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>q(()=>this.config):void 0,this.memorySearch?()=>V(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>J({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>X({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=w(this.config.dataDir);return y({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Y({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Z(this.conceptStore):void 0),S(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new L({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new te(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),V(this.memorySearch);de.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return de.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return de.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};de.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;de.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const i={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},a=`${n}:${o}`;try{const s=await this.handleMessage(i);let n=s;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:o,text:i}=Q(s,t);if(o)return de.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=i}if(t){const t=this.collectBroadcastTargets();de.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,n))),await Promise.allSettled(t.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(e.channel,e.chatId,n),await this.channelManager.releaseTyping(e.channel,e.chatId).catch(()=>{});return{response:n,delivered:!0}}finally{this.agentService.destroySession(a),this.sessionManager.resetSession(a),this.memoryManager&&this.memoryManager.clearSession(a),de.info(`Cron job "${e.name}": ephemeral session destroyed (${a})`)}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const i=o(this.config.dataDir,"HEARTBEAT.md");let a=!1;if(n(i))try{const t=e(i,"utf-8");a=!W(t)}catch{a=!0}const r=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?r?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:a||r?{shouldRun:!0,reason:r?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new T),this.commandRegistry.register(new k);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new $(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,a=re(this.config,i),r=o(a?.name??he(i)),h=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const c=r||h;return c&&this.sessionManager.resetSession(e),c},e)),this.commandRegistry.register(new j(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=ie(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ae(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new E(()=>this.config.models??[],e)),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new U(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new F(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:he(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??he(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?he(s):void 0),fallbackActive:this.agentService.isFallbackActive(e),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new D(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getToolServers())),this.commandRegistry.register(new I(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new O(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new H(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){de.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new g(s,t,this.tokenDb,this.config.agent.inflightTyping);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s,this.config.agent.inflightTyping);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new f({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}if(this.config.channels.mesh.enabled){const e=this.config.channels.mesh;e.agentId&&e.privateKey?(this.meshChannel||(this.meshChannel=new d({brokerUrl:e.brokerUrl,agentId:e.agentId,privateKey:e.privateKey,reconnectDelayMs:e.reconnectDelayMs})),this.channelManager.registerAdapter(this.meshChannel)):de.warn("Mesh channel enabled but agentId or privateKey not configured")}this.webChatChannel||(this.webChatChannel=new m),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";de.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&de.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/"))return this.agentService.isBusy(s)?"I'm busy right now. Please resend this request later.":this.handleMessage({...e,text:e.text,__passthrough:!0});const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),a=b(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);de.debug(`[${s}] Prompt to agent (${a.text.length} chars): ${this.config.verboseDebugLogs?a.text:a.text.slice(0,15)+"..."}${a.images.length>0?` [+${a.images.length} image(s)]`:""}`);const r=o.model,h={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},c=w(this.config.dataDir),g={config:this.config,sessionContext:h,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},l=y(g),m=y({...g,mode:"minimal"});de.debug(`[${s}] System prompt (${l.length} chars): ${this.config.verboseDebugLogs?l:l.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,a,o.sessionId,l,m,r,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return de.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",a={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},r=le.analyzeError(a.error,a),h=le.getRecoveryStrategy(r);return de.warn(`[${s}] ${h.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),h.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==h.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(de.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(de.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(a.text||"[media]").replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2}\]\n?/,"").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",ne(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(de.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(de.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){de.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),this.config.channels.mesh.enabled&&e.push("mesh"),de.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{de.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),de.info("Server started successfully"),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{})}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),de.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),de.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?de.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?de.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):de.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}getMeshChannel(){return this.meshChannel}async triggerRestart(){de.info("Trigger restart requested");const e=oe();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();de.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),await this.channelManager.clearTyping(t.channel,t.chatId),de.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){de.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(de.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>de.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){de.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){de.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}de.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){de.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new u(this.sessionDb),e.memory.enabled?this.memoryManager=new M(e.memoryDir,e.timezone):this.memoryManager=null;const t=C(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new R(t,s),this.commandRegistry=new A,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>z(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new p(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>q(()=>this.config):void 0,this.memorySearch?()=>V(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>J({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>X({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=w(this.config.dataDir);return y({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Y({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Z(this.conceptStore):void 0),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),de.info("Server reconfigured successfully")}async stop(){de.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),de.info("Server stopped")}}
|
|
1
|
+
import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as h}from"./agent/session-db.js";import{ChannelManager as c}from"./gateway/channel-manager.js";import{TypingCoordinator as g}from"./gateway/typing-coordinator.js";import{TelegramChannel as m}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as d}from"./gateway/channels/webchat.js";import{MeshChannel as f}from"./gateway/channels/mesh.js";import{ResponsesChannel as p}from"./channels/responses.js";import{AgentService as u}from"./agent/agent-service.js";import{SessionManager as y}from"./agent/session-manager.js";import{buildPrompt as b,buildSystemPrompt as S}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as w,loadWorkspaceFiles as v}from"./agent/workspace-files.js";import{NodeRegistry as M}from"./gateway/node-registry.js";import{MemoryManager as R}from"./memory/memory-manager.js";import{MessageProcessor as C}from"./media/message-processor.js";import{loadSTTProvider as A}from"./stt/stt-loader.js";import{CommandRegistry as $}from"./commands/command-registry.js";import{NewCommand as T}from"./commands/new.js";import{CompactCommand as k}from"./commands/compact.js";import{ModelCommand as j,DefaultModelCommand as D}from"./commands/model.js";import{StopCommand as I}from"./commands/stop.js";import{HelpCommand as x}from"./commands/help.js";import{McpCommand as E}from"./commands/mcp.js";import{ModelsCommand as _}from"./commands/models.js";import{CoderCommand as N}from"./commands/coder.js";import{SandboxCommand as P}from"./commands/sandbox.js";import{SubAgentsCommand as F}from"./commands/subagents.js";import{CustomSubAgentsCommand as U}from"./commands/customsubagents.js";import{StatusCommand as K}from"./commands/status.js";import{ShowToolCommand as O}from"./commands/showtool.js";import{UsageCommand as H}from"./commands/usage.js";import{DebugA2UICommand as B}from"./commands/debuga2ui.js";import{DebugDynamicCommand as L}from"./commands/debugdynamic.js";import{CronService as z}from"./cron/cron-service.js";import{stripHeartbeatToken as Q,isHeartbeatContentEffectivelyEmpty as W}from"./cron/heartbeat-token.js";import{createServerToolsServer as q}from"./tools/server-tools.js";import{createCronToolsServer as G}from"./tools/cron-tools.js";import{createTTSToolsServer as V}from"./tools/tts-tools.js";import{createMemoryToolsServer as J}from"./tools/memory-tools.js";import{createBrowserToolsServer as X}from"./tools/browser-tools.js";import{createPicoToolsServer as Y}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as Z}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as ee}from"./tools/concept-tools.js";import{BrowserService as te}from"./browser/browser-service.js";import{MemorySearch as se}from"./memory/memory-search.js";import{ConceptStore as ne}from"./memory/concept-store.js";import{stripMediaLines as oe}from"./utils/media-response.js";import{loadConfig as ie,loadRawConfig as re,backupConfig as ae,resolveModelEntry as he,modelRefName as ce}from"./config.js";import{stringify as ge}from"yaml";import{createLogger as me}from"./utils/logger.js";import{SessionErrorHandler as le}from"./agent/session-error-handler.js";import{initStickerCache as de}from"./gateway/channels/telegram/stickers.js";const fe=me("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;typingCoordinator;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;meshChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new h(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new M,this.sessionManager=new y(this.sessionDb),e.memory.enabled&&(this.memoryManager=new R(e.memoryDir,e.timezone));const t=A(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new C(t,n),de(e.dataDir),this.commandRegistry=new $,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.typingCoordinator=new g(this.channelManager),this.serverToolsFactory=()=>q({restartFn:()=>this.triggerRestart(),timezone:this.config.timezone,memoryDir:this.config.memoryDir,warmRestart:this.config.warmRestart}),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new te,this.conceptStore=new ne(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const m=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new u(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>V(()=>this.config):void 0,this.memorySearch?()=>J(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>X({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>Y({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=v(this.config.dataDir);return S({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Z({plasmaRootDir:m,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>ee(this.conceptStore):void 0),w(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new z({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=he(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new se(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),J(this.memorySearch);fe.warn(`Memory search enabled but no API key found for modelRef "${t.modelRef}". Search will not start.`)}stopMemorySearch(){this.memorySearch&&(this.memorySearch.stop(),this.memorySearch=null)}collectBroadcastTargets(){const e=new Set,t=[],s=(s,n)=>{const o=`${s}:${n}`;e.has(o)||(e.add(o),t.push({channel:s,chatId:n}))},n=this.config.channels;for(const[e,t]of Object.entries(n)){if("responses"===e)continue;if(!t?.enabled||!this.channelManager.getAdapter(e))continue;const n=t.accounts;if(n)for(const t of Object.values(n)){const n=t?.allowFrom;if(Array.isArray(n))for(const t of n){const n=String(t).trim();n&&s(e,n)}}}for(const e of this.sessionDb.listSessions()){const t=e.sessionKey.indexOf(":");if(t<0)continue;const n=e.sessionKey.substring(0,t),o=e.sessionKey.substring(t+1);"cron"!==n&&o&&(this.channelManager.getAdapter(n)&&s(n,o))}return t}async executeCronJob(e){const t=this.config.cron.broadcastEvents;if(!t&&!this.channelManager.getAdapter(e.channel))return fe.warn(`Cron job "${e.name}": skipped (channel "${e.channel}" is not active)`),{response:"",delivered:!1};if(e.suppressToken&&"__heartbeat"===e.name){const t=this.triageHeartbeat();if(!t.shouldRun)return fe.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};fe.info(`Cron job "${e.name}": triage passed (${t.reason})`)}const s="boolean"==typeof e.isolated?e.isolated:this.config.cron.isolated,n=s?"cron":e.channel,o=s?e.name:e.chatId;fe.info(`Cron job "${e.name}": session=${n}:${o}, delivery=${e.channel}:${e.chatId}${t?" (broadcast)":""}`);const i={chatId:o,userId:"cron",channelName:n,text:e.message,attachments:[]},r=`${n}:${o}`;try{const s=await this.handleMessage(i);let n=s;if(e.suppressToken){const t=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:o,text:i}=Q(s,t);if(o)return fe.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=i}if(t){const t=this.collectBroadcastTargets();fe.info(`Cron job "${e.name}": broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,n)));for(const e of t)this.typingCoordinator.release(e.channel,e.chatId)}else await this.channelManager.sendResponse(e.channel,e.chatId,n),this.typingCoordinator.release(e.channel,e.chatId);return{response:n,delivered:!0}}finally{this.agentService.destroySession(r),this.sessionManager.resetSession(r),this.memoryManager&&this.memoryManager.clearSession(r),fe.info(`Cron job "${e.name}": ephemeral session destroyed (${r})`)}}triageHeartbeat(){const t=(new Date).getHours(),s=o(this.config.agent.workspacePath,"attention","pending_signals.md");if(n(s))try{const t=e(s,"utf-8").trim().split("\n").filter(e=>e.trim()&&!e.startsWith("#"));if(t.length>0)return{shouldRun:!0,reason:`${t.length} pending signal(s)`}}catch{}const i=o(this.config.dataDir,"HEARTBEAT.md");let r=!1;if(n(i))try{const t=e(i,"utf-8");r=!W(t)}catch{r=!0}const a=this.sessionDb.hasRecentActivity(3e5);return t>=23||t<7?a?{shouldRun:!0,reason:"night mode but recent messages"}:{shouldRun:!1,reason:"night mode, no activity"}:r||a?{shouldRun:!0,reason:a?"recent messages":"actionable heartbeat"}:{shouldRun:!1,reason:"no signals, no messages, empty heartbeat"}}setupCommands(){this.commandRegistry.register(new T),this.commandRegistry.register(new k);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=he(this.config,i),a=o(r?.name??ce(i)),h=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const c=a||h;return c&&this.sessionManager.resetSession(e),c},e)),this.commandRegistry.register(new D(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=re(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ae(e),t(e,ge(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new _(()=>this.config.models??[],e)),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new O(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new U(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new K(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=he(this.config,t),o=s?he(this.config,s):void 0;return{configDefaultModel:ce(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ce(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ce(s):void 0),fallbackActive:this.agentService.isFallbackActive(e),coderSkill:this.getChatSetting(e,"coderSkill")??this.coderSkill,showToolUse:this.getChatSetting(e,"showToolUse")??this.showToolUse,subagentsEnabled:this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,customSubAgentsEnabled:this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,sandboxEnabled:this.getChatSetting(e,"sandboxEnabled")??!1,connectedNodes:this.nodeRegistry.listNodes().map(e=>({nodeId:e.nodeId,displayName:e.displayName,hostname:e.hostname}))}})),this.commandRegistry.register(new I(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new E(()=>this.agentService.getToolServers())),this.commandRegistry.register(new x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new H(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new B(this.nodeRegistry)),this.commandRegistry.register(new L(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){fe.error(`Telegram account "${t}" has no botToken configured — skipping. Check your config.yaml.`);continue}const e=new m(s,t,this.tokenDb);this.channelManager.registerAdapter(e)}}if(this.config.channels.whatsapp.enabled){const e=this.config.channels.whatsapp.accounts;for(const[t,s]of Object.entries(e)){const e=new l(s);e.setQrCallback((e,t,s)=>{this.whatsappQr=e?{dataUrl:e,timestamp:Date.now()}:null,this.whatsappConnected=t,this.whatsappError=s}),this.channelManager.registerAdapter(e)}}if(this.config.channels.responses.enabled){const e=new p({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}if(this.config.channels.mesh.enabled){const e=this.config.channels.mesh;e.agentId&&e.privateKey?(this.meshChannel||(this.meshChannel=new f({brokerUrl:e.brokerUrl,agentId:e.agentId,privateKey:e.privateKey,reconnectDelayMs:e.reconnectDelayMs}),this.meshChannel.onReply((e,t,s)=>{fe.info(`[Mesh] Routing reply from ${t} to origin session ${e}`);const n=e.indexOf(":"),o=n>0?e.substring(0,n):"mesh",i=n>0?e.substring(n+1):t,r={chatId:i,userId:t,channelName:o,text:`[Mesh reply from ${t}] ${s}`,attachments:[],username:t};this.handleMessage(r).then(t=>{t&&t.trim()&&this.channelManager.sendResponse(o,i,t).catch(t=>{fe.error(`[Mesh] Failed to deliver reply-response to ${e}: ${t}`)})}).catch(s=>{fe.error(`[Mesh] Failed to process reply from ${t} in session ${e}: ${s}`)})})),this.channelManager.registerAdapter(this.meshChannel)):fe.warn("Mesh channel enabled but agentId or privateKey not configured")}this.webChatChannel||(this.webChatChannel=new d),this.channelManager.registerAdapter(this.webChatChannel)}async handleMessage(e,t=!1){const s=`${e.channelName}:${e.chatId}`,n=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";fe.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&fe.debug(`Message from ${s} full text: ${e.text??"[no text]"}`);try{if(this.typingCoordinator.acquire(e.channelName,e.chatId),e.text){if(e.text.startsWith("__ask:")){const t=e.text.substring(6);return this.agentService.resolveQuestion(s,t),""}if(this.agentService.hasPendingQuestion(s)){const t=e.text.trim();return this.agentService.resolveQuestion(s,t),`Selected: ${t}`}if(e.text.startsWith("__tool_perm:")){const t="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(s,t),t?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(s)){const t=e.text.trim().toLowerCase();if("approve"===t||"approva"===t)return this.agentService.resolvePermission(s,!0),"Tool approved.";if("deny"===t||"vieta"===t||"blocca"===t)return this.agentService.resolvePermission(s,!1),"Tool denied."}}const n=!0===e.__passthrough;if(!n&&e.text&&this.commandRegistry.isCommand(e.text)){const t=await this.commandRegistry.dispatch(e.text,{sessionKey:s,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(t)return t.passthrough?this.handleMessage({...e,text:t.passthrough,__passthrough:!0}):(t.resetSession?(this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s)):t.resetAgent&&this.agentService.destroySession(s),t.buttons&&t.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,t.text,t.buttons),""):t.text)}if(!n&&e.text?.startsWith("/"))return this.agentService.isBusy(s)?"I'm busy right now. Please resend this request later.":this.handleMessage({...e,text:e.text,__passthrough:!0});const o=this.sessionManager.getOrCreate(s),i=await this.messageProcessor.process(e),r=b(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);fe.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,h={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},c=v(this.config.dataDir),g={config:this.config,sessionContext:h,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},m=S(g),l=S({...g,mode:"minimal"});fe.debug(`[${s}] System prompt (${m.length} chars): ${this.config.verboseDebugLogs?m:m.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,m,l,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return fe.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=le.analyzeError(r.error,r),h=le.getRecoveryStrategy(a);return fe.warn(`[${s}] ${h.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),h.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==h.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(fe.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}fe.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId);const h="cron"===e.userId,c=!0===this.config.cron.trackMemory;if(this.memoryManager&&(!h||c)){const e=(r.text||"[media]").replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2}\]\n?/,"").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",oe(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(fe.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(fe.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{this.typingCoordinator.release(e.channelName,e.chatId)}}async start(){fe.info("Starting GrabMeABeer server...");const e=[];this.config.channels.telegram.enabled&&e.push("telegram"),this.config.channels.responses.enabled&&e.push("responses"),this.config.channels.whatsapp.enabled&&e.push("whatsapp"),this.config.channels.discord.enabled&&e.push("discord"),this.config.channels.slack.enabled&&e.push("slack"),this.config.channels.mesh.enabled&&e.push("mesh"),fe.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),this.notifyAllChannels("Gateway started. Agent is alive!").catch(()=>{}),await this.browserService.start(this.config.browser).catch(e=>{fe.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),fe.info("Server started successfully")}async initCronAndHeartbeat(){if(!this.cronService)return;await this.cronService.start();const e=this.config.cron.heartbeat,t=(await this.cronService.list({includeDisabled:!0})).find(e=>"__heartbeat"===e.name),s=!!e.channel&&!!this.channelManager.getAdapter(e.channel),n=!!e.message&&e.message.trim().length>=15;if(e.enabled&&e.channel&&e.chatId&&s&&n){const s={schedule:{kind:"every",everyMs:e.every},channel:e.channel,chatId:e.chatId,message:e.message};if(t){const e=t.schedule;(t.channel!==s.channel||t.chatId!==s.chatId||t.message!==s.message||t.isolated!==this.config.cron.isolated||"every"!==e.kind||"every"===e.kind&&e.everyMs!==s.schedule.everyMs||!t.enabled)&&(await this.cronService.update(t.id,{...s,isolated:this.config.cron.isolated,enabled:!0}),fe.info("Heartbeat job updated from config"))}else await this.cronService.add({name:"__heartbeat",description:"Auto-generated heartbeat job",enabled:!0,isolated:this.config.cron.isolated,suppressToken:!0,...s}),fe.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?fe.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?fe.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):fe.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}getMeshChannel(){return this.meshChannel}async triggerRestart(){fe.info("Trigger restart requested");const e=ie();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();fe.info(`Broadcasting to ${t.length} target(s)`),await Promise.allSettled(t.map(async t=>{try{await this.channelManager.sendSystemMessage(t.channel,t.chatId,e),this.typingCoordinator.reinforceIfActive(t.channel,t.chatId),fe.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){fe.warn(`Failed to notify ${t.channel}:${t.chatId}: ${e}`)}}))}static AUTO_RENEW_CHECK_INTERVAL_MS=9e5;startAutoRenewTimer(){this.stopAutoRenewTimer();const e=this.config.agent.autoRenew;e&&(fe.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>fe.error(`AutoRenew error: ${e}`))},Server.AUTO_RENEW_CHECK_INTERVAL_MS))}stopAutoRenewTimer(){this.autoRenewTimer&&(clearInterval(this.autoRenewTimer),this.autoRenewTimer=null)}async autoRenewStaleSessions(){const e=this.config.agent.autoRenew;if(!e)return;const t=60*e*60*1e3,s=this.sessionDb.listStaleSessions(t);if(0!==s.length){fe.info(`AutoRenew: found ${s.length} stale session(s)`);for(const t of s){const s=t.sessionKey;if(s.startsWith("cron:"))continue;if(this.agentService.isBusy(s))continue;this.agentService.destroySession(s),this.sessionManager.resetSession(s),this.memoryManager&&this.memoryManager.clearSession(s);const n=s.indexOf(":");if(n>0){const t=s.substring(0,n),o=s.substring(n+1),i=this.channelManager.getAdapter(t);if(i)try{await i.sendText(o,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){fe.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}fe.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){fe.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new y(this.sessionDb),e.memory.enabled?this.memoryManager=new R(e.memoryDir,e.timezone):this.memoryManager=null;const t=A(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new C(t,s),this.commandRegistry=new $,this.setupCommands(),this.channelManager=new c(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.typingCoordinator=new g(this.channelManager),this.agentService.destroyAll(),this.serverToolsFactory=()=>q({restartFn:()=>this.triggerRestart(),timezone:this.config.timezone,memoryDir:this.config.memoryDir,warmRestart:this.config.warmRestart}),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new u(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>G(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>V(()=>this.config):void 0,this.memorySearch?()=>J(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>X({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>Y({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=v(this.config.dataDir);return S({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>Z({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>ee(this.conceptStore):void 0),w(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),fe.info("Server reconfigured successfully")}async stop(){fe.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),this.conceptStore&&this.conceptStore.close(),await this.browserService.stop(),this.typingCoordinator.stopAll(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),fe.info("Server stopped")}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppConfig } from "../config.js";
|
|
2
2
|
import type { ChannelManager } from "../gateway/channel-manager.js";
|
|
3
3
|
import type { SessionDB } from "../agent/session-db.js";
|
|
4
|
-
export declare function createMessageToolsServer(channelManager: ChannelManager, getConfig: () => AppConfig, sessionDb: SessionDB): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
4
|
+
export declare function createMessageToolsServer(channelManager: ChannelManager, getConfig: () => AppConfig, sessionDb: SessionDB, originSessionKey?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
5
5
|
//# sourceMappingURL=message-tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as s}from"../utils/logger.js";const a=s("MessageTools");export function createMessageToolsServer(s,o,r,i){return e({name:"message-tools",version:"1.0.0",tools:[t("send_message","Send a text message to a chat on a specific channel. Use the channel and chatId from <session_info> to reply on the current conversation, or specify a different channel/chatId to send elsewhere.",{channel:n.string().describe("The channel name to send to (e.g. 'telegram', 'whatsapp', 'responses')"),chatId:n.string().describe("The chat ID to send to (from <session_info> or another known chat)"),text:n.string().describe("The message text to send"),buttons:n.array(n.object({text:n.string().describe("Button label"),callbackData:n.string().optional().describe("Data sent back when button is tapped (defaults to text)"),url:n.string().optional().describe("URL to open when button is tapped (mutually exclusive with callbackData)")})).optional().describe("Optional inline buttons to attach to the message")},async e=>{try{if(e.buttons&&e.buttons.length>0)await s.sendButtons(e.channel,e.chatId,e.text,[e.buttons]);else if("mesh"===e.channel&&i){const t=s.getAdapter("mesh");t?await t.sendText(e.chatId,e.text,i):await s.sendResponse(e.channel,e.chatId,e.text)}else await s.sendResponse(e.channel,e.chatId,e.text);return a.info(`Message sent to ${e.channel}:${e.chatId} (${e.text.length} chars)`),{content:[{type:"text",text:`Message sent to ${e.channel}:${e.chatId}`}]}}catch(t){const n=t instanceof Error?t.message:String(t);return a.error(`Failed to send message to ${e.channel}:${e.chatId}: ${n}`),{content:[{type:"text",text:`Error sending message: ${n}`}],isError:!0}}}),t("list_models","List all models in the registry with their name, model ID, API base URL, and API key environment variable name.",{},async()=>{const e=(o().models||[]).filter(e=>{const t=e.types||["external"];return t.includes("internal")||t.includes("external")&&e.proxy&&"not-used"!==e.proxy});if(0===e.length)return{content:[{type:"text",text:"No models in registry."}]};const t=e.map(e=>({name:e.name,modelId:e.id,type:(e.types||["external"])[0],baseURL:e.baseURL||"",apiKeyEnvVar:e.useEnvVar||((e.types||["external"]).includes("internal")?"":"OPENAI_API_KEY")}));return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}),t("list_channels","List all registered channels, whether they are currently active, and known chat IDs for each channel (from config allowFrom + session history).",{},async()=>{const e=s.listAdapters();if(0===e.length)return{content:[{type:"text",text:"No channels registered."}]};const t=o(),n=r.listSessions(),a=e.map(e=>{const s=new Set,a=[],o=t.channels[e.name];if(o?.accounts)for(const e of Object.values(o.accounts)){const t=e?.allowFrom;if(Array.isArray(t))for(const e of t){const t=String(e).trim();t&&!s.has(t)&&(s.add(t),a.push({id:t,source:"config"}))}}for(const t of n){const n=t.sessionKey.indexOf(":");if(n<0)continue;const o=t.sessionKey.substring(0,n),r=t.sessionKey.substring(n+1);o===e.name&&r&&!s.has(r)&&(s.add(r),a.push({id:r,source:"session"}))}return{name:e.name,active:e.active,chatIds:a}});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]}})]})}
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
interface ServerToolsOptions {
|
|
2
|
+
restartFn: () => Promise<void>;
|
|
3
|
+
timezone?: string;
|
|
4
|
+
memoryDir?: string;
|
|
5
|
+
warmRestart?: {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
pm2Name: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function createServerToolsServer(opts: ServerToolsOptions): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
11
|
+
export {};
|
|
2
12
|
//# sourceMappingURL=server-tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{
|
|
1
|
+
import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{appendFileSync as r,existsSync as o,mkdirSync as n}from"node:fs";import{join as i}from"node:path";import{execSync as s}from"node:child_process";import{createLogger as a}from"../utils/logger.js";const m=a("ServerTools");function c(e,t,s){if(!e)return;const a=i(e,"RESET_HISTORY.md"),c=new Date,d=s?c.toLocaleString("en-GB",{timeZone:s,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}):c.toISOString();o(a)||(n(e,{recursive:!0}),r(a,"# Reset History\n\n| Timestamp | Type |\n|---|---|\n","utf-8")),r(a,`| ${d} | ${t} |\n`,"utf-8"),m.info(`Logged ${t} restart to ${a}`)}export function createServerToolsServer(r){const{restartFn:o,timezone:n,memoryDir:i,warmRestart:a}=r,d=n||Intl.DateTimeFormat().resolvedOptions().timeZone,l=[t("restart_server","Restart the GrabMeABeer server (cold restart). This reloads config from disk and reinitializes all channels, agents, and services. Active conversations will be interrupted. Use this when you need to apply configuration changes or recover from issues.",{},async()=>(m.info("Cold restart triggered by agent"),c(i,"cold",n),setTimeout(()=>{o().catch(e=>{const t=e instanceof Error?e.message:String(e);m.error(`Agent-triggered cold restart failed: ${t}`)})},100),{content:[{type:"text",text:"Server cold restart initiated. The server will reconfigure momentarily."}]})),t("get_current_time","Get the current date and time in the server's configured timezone. Use this tool whenever you need to know the current time, date, day of the week, or any time-related information. Do not guess or estimate the current time — always call this tool.",{},async()=>{const e=new Date,t=e.toLocaleString("en-GB",{timeZone:d,weekday:"long",year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),r=e.toISOString();return{content:[{type:"text",text:`Current time (${d}): ${t}\nISO 8601: ${r}\nUnix timestamp: ${Math.floor(e.getTime()/1e3)}`}]}})];return a?.enabled&&a.pm2Name&&l.push(t("restart_server_warm","Warm restart the server via PM2. The process is killed and respawned by PM2. Use this when code changes require a full process restart (not just config reload). Requires warmRestart to be enabled in config.",{},async()=>{const e=a.pm2Name;return m.info(`Warm restart (PM2) triggered by agent for process: ${e}`),c(i,"warm",n),setTimeout(()=>{try{s(`pm2 restart ${e}`,{timeout:1e4})}catch(e){const t=e instanceof Error?e.message:String(e);m.error(`PM2 warm restart failed: ${t}`)}},100),{content:[{type:"text",text:`Warm restart initiated via PM2 (process: ${e}). The server will respawn momentarily.`}]}})),e({name:"server-tools",version:"1.0.0",tools:l})}
|
|
@@ -157,6 +157,10 @@ Cron: exact timing, isolation, one-shot reminders, heavy work.
|
|
|
157
157
|
|
|
158
158
|
When multiple skills could handle a task, prefer the one with lower priority number.
|
|
159
159
|
|
|
160
|
+
## Server Restart
|
|
161
|
+
|
|
162
|
+
Restart history is logged in **`memory/RESET_HISTORY.md`** (timestamp + type cold/warm).
|
|
163
|
+
|
|
160
164
|
## Make It Yours
|
|
161
165
|
|
|
162
166
|
This is a starting point. Add your own conventions, style, and rules as you figure out what works.
|
|
@@ -1,92 +1,111 @@
|
|
|
1
|
-
# BEHAVIOUR.md — Operational
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# BEHAVIOUR.md v2 — Operational Rules
|
|
2
|
+
# Format: each rule has priority (1=critical), trigger, actions
|
|
3
|
+
# Rules are ordered by priority, not by position in file
|
|
4
|
+
# Single source of truth. Dreaming adds here, not elsewhere.
|
|
5
|
+
#
|
|
6
|
+
# HOW TO ADD A NEW RULE:
|
|
7
|
+
# 1. Pick the right priority level (P1=integrity, P2=persistence, P3=session, etc.)
|
|
8
|
+
# 2. Define the trigger (when does this rule fire?)
|
|
9
|
+
# 3. Write atomic actions (do/never) — if it's hard to follow, it won't be followed
|
|
10
|
+
# 4. Add a reason if there's a specific incident that motivated this rule
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# - trigger: user-mentions-price
|
|
14
|
+
# priority: 4
|
|
15
|
+
# do: state_get to check current value BEFORE responding
|
|
16
|
+
# never: quote prices from memory — they change
|
|
6
17
|
|
|
7
18
|
---
|
|
19
|
+
# P1 — INTEGRITY (violation = credibility penalty)
|
|
20
|
+
|
|
21
|
+
- trigger: always
|
|
22
|
+
priority: 1
|
|
23
|
+
do: VERIFY before asserting checkable facts (date, time, count, existence)
|
|
24
|
+
never: present unverified assumptions as facts — say "I'm not sure" instead
|
|
25
|
+
|
|
26
|
+
- trigger: always
|
|
27
|
+
priority: 1
|
|
28
|
+
do: "I don't know" when you don't know
|
|
29
|
+
never: lie, embellish, pretend
|
|
30
|
+
|
|
31
|
+
- trigger: always
|
|
32
|
+
priority: 1
|
|
33
|
+
never:
|
|
34
|
+
- guessing dates/times from memory — always verify with tool
|
|
35
|
+
- single command string in node_exec (ALWAYS cmd + args separated)
|
|
36
|
+
- polling background task output files (WAIT for task-notification instead)
|
|
37
|
+
- repeating the same action with slight wording variations (= loop)
|
|
8
38
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- VERIFY before asserting any checkable fact (date, time, day, count, existence, negation)
|
|
12
|
-
- NEVER lie, embellish, or pretend — "I don't know" > inventing an answer
|
|
13
|
-
- concept_draft on every new person, fact, relation, or decision that emerges
|
|
14
|
-
- When the user corrects → update BEHAVIOUR.md or relevant file IMMEDIATELY (one correction = permanent fix)
|
|
15
|
-
- When new info from user → update memory files immediately (MEMORY.md, memory/), not just concept_draft
|
|
16
|
-
|
|
17
|
-
## NEVER — Hard prohibitions
|
|
18
|
-
|
|
19
|
-
- Comparative claims without systematic search ("nobody does X", "you're the only one")
|
|
20
|
-
- Invented metadata (unknown origin → don't add it)
|
|
21
|
-
- Guessing dates/times from memory — always verify with tool
|
|
22
|
-
- Single command string in node_exec (ALWAYS cmd + args separated)
|
|
23
|
-
|
|
24
|
-
## WHEN:session-start — First message of the conversation
|
|
25
|
-
|
|
26
|
-
If the message is the first in the session (no previous messages in context):
|
|
27
|
-
|
|
28
|
-
1. `get_current_time` (always)
|
|
29
|
-
2. Use `memory_list` with today and yesterday's dates, then `memory_get` to read the matching files (if any)
|
|
30
|
-
3. If the message assumes shared context (people, projects, events, pronouns without referent):
|
|
31
|
-
- `memory_search` with keywords from the message
|
|
32
|
-
- `concept_query` on mentioned entities (in parallel)
|
|
33
|
-
4. If the message is self-contained ("what time is it", "translate X", generic technical question) → skip 3, respond directly
|
|
34
|
-
|
|
35
|
-
Goal: reconstruct context BEFORE responding, not after saying something wrong.
|
|
36
|
-
|
|
37
|
-
## WHEN:entity-mentioned-without-context — Context gap
|
|
38
|
-
|
|
39
|
-
- memory_search + concept_query IN PARALLEL (anti-underuse graph)
|
|
40
|
-
- Do NOT apply to technical/operative searches (config, errors, how-to) — graph has nothing there
|
|
41
|
-
- If references to unknown people/events → search IMMEDIATELY, don't ask for clarification
|
|
42
|
-
|
|
43
|
-
## WHEN:memory-write — Writing to persistent files
|
|
44
|
-
|
|
45
|
-
- Store conclusions not reasoning, facts not narratives
|
|
46
|
-
- One line per fact when possible — scannable beats readable
|
|
47
|
-
- Dead memory is misleading — delete or update stale entries, don't accumulate
|
|
48
|
-
|
|
49
|
-
## WHEN:memory-retrieval — Searching past context
|
|
50
|
-
|
|
51
|
-
- memory_search → relevant snippet → memory_expand (activates RL tracking automatically)
|
|
52
|
-
- memory_record_access manual only when snippet was sufficient but decisive
|
|
53
|
-
- concept_query for navigating relationships (depth=2 for "all X of Y")
|
|
54
|
-
|
|
55
|
-
## WHEN:mutable-data — State registry vs memory
|
|
39
|
+
---
|
|
40
|
+
# P2 — CORRECTIONS & PERSISTENCE
|
|
56
41
|
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
- trigger: user-corrects
|
|
43
|
+
priority: 2
|
|
44
|
+
do: update BEHAVIOUR.md or relevant file IMMEDIATELY
|
|
45
|
+
rule: one correction = permanent fix, no repeat offenses
|
|
60
46
|
|
|
61
|
-
|
|
47
|
+
- trigger: new-info-from-user
|
|
48
|
+
priority: 2
|
|
49
|
+
do: update memory files immediately (MEMORY.md, memory/)
|
|
62
50
|
|
|
63
|
-
-
|
|
64
|
-
|
|
51
|
+
- trigger: new-entity-observed
|
|
52
|
+
priority: 2
|
|
53
|
+
do: concept_draft on every new person, fact, relation, decision
|
|
65
54
|
|
|
66
|
-
|
|
55
|
+
---
|
|
56
|
+
# P3 — SESSION LIFECYCLE
|
|
57
|
+
|
|
58
|
+
- trigger: session-start
|
|
59
|
+
priority: 3
|
|
60
|
+
do:
|
|
61
|
+
1. get_current_time (always)
|
|
62
|
+
2. memory_list today+yesterday → memory_get to read found files
|
|
63
|
+
3. if message needs shared context → memory_search + concept_query (parallel)
|
|
64
|
+
4. if self-contained → skip 3, respond directly
|
|
65
|
+
goal: reconstruct context BEFORE responding
|
|
66
|
+
|
|
67
|
+
- trigger: entity-mentioned-without-context
|
|
68
|
+
priority: 3
|
|
69
|
+
do: memory_search + concept_query IN PARALLEL
|
|
70
|
+
never: ask for clarification before searching
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- NEVER share the user's private info from MEMORY.md
|
|
72
|
+
---
|
|
73
|
+
# P4 — MEMORY & STATE
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
- trigger: memory-write
|
|
76
|
+
priority: 4
|
|
77
|
+
do: store conclusions not reasoning, facts not narratives, one line per fact
|
|
78
|
+
never: accumulate stale entries — delete or update dead memory
|
|
73
79
|
|
|
74
|
-
-
|
|
80
|
+
- trigger: mutable-data
|
|
81
|
+
priority: 4
|
|
82
|
+
state_registry: prices, counters, task status, current focus
|
|
83
|
+
memory_files: stable knowledge, decisions, history
|
|
84
|
+
concept_graph: structured relationships
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
---
|
|
87
|
+
# P5 — CHANNELS & CONTEXT
|
|
77
88
|
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
-
|
|
89
|
+
- trigger: external-action
|
|
90
|
+
priority: 5
|
|
91
|
+
ask-before: sending emails, posts, anything reaching third parties
|
|
92
|
+
do-freely: read files, search web, organize workspace, research
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
- trigger: cron-job
|
|
95
|
+
priority: 5
|
|
96
|
+
rules:
|
|
97
|
+
- message must be SELF-CONTAINED
|
|
98
|
+
- cronExpr is UTC! For 07:00 Rome (CET) use "0 6 * * *"
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
---
|
|
101
|
+
# P6 — DOMAIN-SPECIFIC
|
|
102
|
+
# Add rules here for your agent's specific domains (health, finance, coding, etc.)
|
|
86
103
|
|
|
87
|
-
|
|
104
|
+
---
|
|
105
|
+
# META — How rules work
|
|
88
106
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
107
|
+
- cognitive cost alto → rule ignored. Make actions atomic.
|
|
108
|
+
- "sounds right" ≠ "is right" — pattern matching is the enemy of accuracy
|
|
109
|
+
- simplicity > complexity
|
|
110
|
+
- contrarian frame: define what NOT to do
|
|
111
|
+
- WHY before HOW
|
|
@@ -124,6 +124,16 @@ channels:
|
|
|
124
124
|
# userId: ""
|
|
125
125
|
# accessToken: ""
|
|
126
126
|
|
|
127
|
+
# mesh: # inter-agent mesh communication
|
|
128
|
+
# enabled: false
|
|
129
|
+
# brokerUrl: "ws://127.0.0.1:3780/ws" # mesh broker WebSocket URL
|
|
130
|
+
# agentId: "" # unique agent ID on the mesh
|
|
131
|
+
# privateKey: "${MESH_KEY}" # Ed25519 private key for mesh auth
|
|
132
|
+
# reconnectDelayMs: 5000 # delay before reconnecting on disconnect
|
|
133
|
+
# rateLimit: # per-peer rate limiting (anti-loop, inspired by "Agents of Chaos" paper)
|
|
134
|
+
# maxPerPair: 20 # max outbound messages per peer per window
|
|
135
|
+
# windowMs: 3600000 # sliding window in ms (default: 1 hour)
|
|
136
|
+
|
|
127
137
|
responses:
|
|
128
138
|
enabled: true
|
|
129
139
|
port: 3002
|
|
@@ -229,7 +239,7 @@ models: # model registry
|
|
|
229
239
|
costOutput: 0
|
|
230
240
|
costCacheRead: 0
|
|
231
241
|
costCacheWrite: 0
|
|
232
|
-
- id: "openrouter:x-ai/grok-4.
|
|
242
|
+
- id: "openrouter:x-ai/grok-4.20-beta"
|
|
233
243
|
name: OpenRouter Grok
|
|
234
244
|
types: [external]
|
|
235
245
|
apiKey: "${OPENROUTER_API_KEY}"
|
|
@@ -304,7 +314,7 @@ memory:
|
|
|
304
314
|
recallStrategy: "search" # builtin-only | search
|
|
305
315
|
search:
|
|
306
316
|
enabled: true # enable hybrid BM25 + semantic search over memory
|
|
307
|
-
modelRef: "Embedding Gemma"
|
|
317
|
+
modelRef: "Embedding Gemma:embeddinggemma" # "Name:id" reference to a model in the models registry (for API key + base URL)
|
|
308
318
|
embeddingModel: "embeddinggemma" # Ollama model (run: ollama pull gemma)
|
|
309
319
|
prefixQuery: "task: search result | query: {content}" # prefix template for query embeddings (use {content} placeholder)
|
|
310
320
|
prefixDocument: "title: none | text: {content}" # prefix template for document embeddings (use {content} placeholder)
|
|
@@ -338,7 +348,7 @@ agent:
|
|
|
338
348
|
modelRefs: # list of "Name:provider:modelId" refs
|
|
339
349
|
- "OpenRouter GPT:openrouter:openai/gpt-5.4"
|
|
340
350
|
- "OpenRouter Flash:openrouter:google/gemini-3-flash-preview"
|
|
341
|
-
- "OpenRouter Grok:openrouter:x-ai/grok-4.
|
|
351
|
+
- "OpenRouter Grok:openrouter:x-ai/grok-4.20-beta"
|
|
342
352
|
- "OpenRouter GeminiPro:openrouter:google/gemini-3-pro-preview"
|
|
343
353
|
- "OpenRouter Opus1m:openrouter:anthropic/claude-opus-4.6"
|
|
344
354
|
rollingMemoryModel: "OpenRouter Flash:openrouter:google/gemini-3-flash-preview"
|
|
@@ -473,6 +483,7 @@ cron:
|
|
|
473
483
|
enabled: true
|
|
474
484
|
isolated: true # jobs run in their own session (cron:name) instead of sharing user chat context
|
|
475
485
|
broadcastEvents: false # deliver cron responses to all known chats across all active channels
|
|
486
|
+
trackMemory: false # save cron session conversations to memory files
|
|
476
487
|
# storePath: "" # leave empty for default (data/cron/jobs.json)
|
|
477
488
|
heartbeat:
|
|
478
489
|
enabled: true
|
|
@@ -482,6 +493,10 @@ cron:
|
|
|
482
493
|
message: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK."
|
|
483
494
|
ackMaxChars: 300
|
|
484
495
|
|
|
496
|
+
warmRestart:
|
|
497
|
+
enabled: false # enable warm restart via PM2 (kill + respawn)
|
|
498
|
+
pm2Name: "" # PM2 process name (e.g. "dante-server")
|
|
499
|
+
|
|
485
500
|
nostromo:
|
|
486
501
|
enabled: true
|
|
487
502
|
port: 3001
|
|
@@ -492,3 +507,16 @@ nostromo:
|
|
|
492
507
|
browser:
|
|
493
508
|
enabled: false
|
|
494
509
|
controlPort: 3003
|
|
510
|
+
headless: false # run Chrome headless (no visible window)
|
|
511
|
+
noSandbox: false # disable Chrome sandbox (needed in some Docker setups)
|
|
512
|
+
attachOnly: false # attach to existing Chrome instead of launching one
|
|
513
|
+
# executablePath: "/usr/bin/google-chrome" # custom Chrome binary path (optional)
|
|
514
|
+
remoteCdpTimeoutMs: 1500 # timeout for remote CDP connections
|
|
515
|
+
profiles: # named browser profiles with separate CDP ports
|
|
516
|
+
default:
|
|
517
|
+
cdpPort: 9222
|
|
518
|
+
color: "#FF4500"
|
|
519
|
+
# secondary:
|
|
520
|
+
# cdpPort: 9223
|
|
521
|
+
# cdpUrl: "http://remote-host:9222" # or use cdpUrl for remote browsers
|
|
522
|
+
# color: "#00FF00"
|