@hera-al/server 1.6.35 → 1.6.37

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/config.d.ts CHANGED
@@ -266,6 +266,13 @@ declare const AppConfigSchema: z.ZodObject<{
266
266
  enabled: z.ZodDefault<z.ZodBoolean>;
267
267
  accounts: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodAny>>;
268
268
  }, z.core.$strip>>>;
269
+ mesh: z.ZodDefault<z.ZodOptional<z.ZodObject<{
270
+ enabled: z.ZodDefault<z.ZodBoolean>;
271
+ brokerUrl: z.ZodDefault<z.ZodString>;
272
+ agentId: z.ZodDefault<z.ZodString>;
273
+ privateKey: z.ZodDefault<z.ZodString>;
274
+ reconnectDelayMs: z.ZodDefault<z.ZodNumber>;
275
+ }, z.core.$strip>>>;
269
276
  responses: z.ZodDefault<z.ZodOptional<z.ZodObject<{
270
277
  enabled: z.ZodDefault<z.ZodBoolean>;
271
278
  port: z.ZodDefault<z.ZodNumber>;
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,existsSync as a,mkdirSync as o,renameSync as n,unlinkSync as l}from"node:fs";import{resolve as r,join as s}from"node:path";import{homedir as i}from"node:os";import{config as u}from"dotenv";import{parse as d,stringify as c}from"yaml";import{z as f}from"zod";import{BrowserConfigSchema as p}from"@hera-al/browser-server/config";import{createLogger as m}from"./utils/logger.js";const b=m("Config");function g(e){return"~"===e||e.startsWith("~/")?e.replace("~",i()):e}let h=g(process.env.GMAB_PATH??"~/gmab"),y=s(h,"data");export function getGmabPath(){return h}export function getDataDir(){return y}export function getNostromoKeyPath(){return s(y,".nostromo-key")}export function backupConfig(o){if(a(o))try{const r=e=>`${o}.backup${e}`;a(r(1))&&l(r(1));for(let e=2;e<=5;e++)a(r(e))&&n(r(e),r(e-1));t(r(5),e(o)),b.debug(`Config backup created: ${r(5)}`)}catch(e){b.warn(`Failed to create config backup: ${e}`)}}const v=f.record(f.string(),f.any()),x=f.object({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({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:{}}),responses:R.optional().default({enabled:!0,port:3e3})}),S=f.object({modelRef:f.string().default(""),model:f.string().default("whisper-1"),language:f.string().default("")}),T=f.object({binaryPath:f.string().default("whisper"),model:f.string().default("base")}),A=f.object({enabled:f.boolean().default(!1),provider:f.string().default("openai-whisper"),"openai-whisper":S.optional().default({modelRef:"",model:"whisper-1",language:""}),"local-whisper":T.optional().default({binaryPath:"whisper",model:"base"})}),D=f.object({voice:f.string().default("en-US-MichelleNeural"),lang:f.string().default("en-US"),outputFormat:f.string().default("audio-24khz-48kbitrate-mono-mp3")}),I=f.object({modelRef:f.string().default(""),model:f.string().default("gpt-4o-mini-tts"),voice:f.string().default("alloy")}),U=f.object({modelRef:f.string().default(""),voiceId:f.string().default("pMsXgVXv3BLzUgSXRplE"),modelId:f.string().default("eleven_multilingual_v2")}),z=f.object({enabled:f.boolean().default(!1),provider:f.enum(["edge","openai","elevenlabs"]).default("openai"),maxTextLength:f.number().default(4096),timeoutMs:f.number().default(3e4),edge:D.optional().default({voice:"en-US-MichelleNeural",lang:"en-US",outputFormat:"audio-24khz-48kbitrate-mono-mp3"}),openai:I.optional().default({modelRef:"",model:"gpt-4o-mini-tts",voice:"alloy"}),elevenlabs:U.optional().default({modelRef:"",voiceId:"pMsXgVXv3BLzUgSXRplE",modelId:"eleven_multilingual_v2"})}),L=f.object({enabled:f.boolean().default(!1),embeddingModel:f.string().default("text-embedding-3-small"),embeddingDimensions:f.number().default(1536),modelRef:f.string().default(""),prefixQuery:f.string().default(""),prefixDocument:f.string().default(""),updateDebounceMs:f.number().default(3e3),embedIntervalMs:f.number().default(3e5),maxResults:f.number().default(6),maxSnippetChars:f.number().default(700),maxInjectedChars:f.number().default(4e3),rrfK:f.number().default(60)}),W={enabled:!1,embeddingModel:"text-embedding-3-small",embeddingDimensions:1536,modelRef:"",prefixQuery:"",prefixDocument:"",updateDebounceMs:3e3,embedIntervalMs:3e5,maxResults:6,maxSnippetChars:700,maxInjectedChars:4e3,rrfK:60},E=f.object({enabled:f.boolean().default(!0),model:f.string().default("")}),F={enabled:!0,model:""},K=f.object({enabled:f.boolean().default(!0),recallStrategy:f.enum(["builtin-only","search"]).default("builtin-only"),search:L.optional().default(W),l0:E.optional().default(F)}),q=f.object({command:f.string(),args:f.array(f.string()).optional(),env:f.record(f.string(),f.string()).optional()}).passthrough(),G=f.object({id:f.string(),name:f.string(),types:f.array(f.enum(["internal","external","env-var"])).optional().default(["external"]),proxy:f.enum(["not-used","direct","proxied"]).optional().default("not-used"),fastUrl:f.string().optional().default(""),fastProxyApiKey:f.string().optional().default(""),apiKey:f.string().optional().default(""),baseURL:f.string().optional().default(""),useEnvVar:f.string().optional().default(""),contextWindow:f.number().optional().default(2e5),costInput:f.number().optional().default(0),costOutput:f.number().optional().default(0),costCacheRead:f.number().optional().default(0),costCacheWrite:f.number().optional().default(0)}),O=[{id:"claude-opus-4-6",name:"Claude Opus",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-sonnet-4-6",name:"Claude Sonnet",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku",types:["internal"],proxy:"not-used",fastUrl:"",fastProxyApiKey:"",apiKey:"",baseURL:"",useEnvVar:"",contextWindow:2e5,costInput:0,costOutput:0,costCacheRead:0,costCacheWrite:0}],_=f.array(G).default(O),B=f.object({name:f.string(),path:f.string(),description:f.string().default(""),enabled:f.boolean().default(!1)}),N=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)}),X=f.object({type:f.enum(["claudecode","pi"]).default("claudecode"),piModelRef:f.string().default("")}).passthrough(),$={type:"claudecode",piModelRef:""},V=f.object({enabled:f.boolean().default(!1),modelRefs:f.array(f.string()).default([]),rollingMemoryModel:f.string().default("")}),H={enabled:!1,modelRefs:[],rollingMemoryModel:""},Q=f.object({maxAttempts:f.number().default(5),baseDelayMs:f.number().default(2e3),maxDelayMs:f.number().default(3e4)}),Z={maxAttempts:5,baseDelayMs:2e3,maxDelayMs:3e4},J=f.object({model:f.string().default("claude-opus-4-6"),mainFallback:f.string().default(""),engine:X.optional().default($),picoAgent:V.optional().default(H),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(),q).default({}),workspacePath:f.string().default("./workspace"),builtinCoderSkill:f.boolean().default(!1),settingSources:f.enum(["nothing","user","project","both"]).default("project"),customSubAgents:f.array(N).default([]),plugins:f.array(B).default([]),inflightTyping:f.boolean().default(!0),autoApproveTools:f.boolean().default(!0),autoRenew:f.number().default(0),apiRetry:Q.optional().default(Z),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})}),Y={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"}},ee={enabled:!0,recallStrategy:"builtin-only",dir:"",search:W,l0:F},te={model:"claude-opus-4-6",mainFallback:"",engine:$,picoAgent:H,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:Z,skillNudge:{enabled:!0,threshold:10},qualityGate:{enabled:!1}},ae=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)}),oe={enabled:!1,every:18e5,channel:"",chatId:"",message:"",ackMaxChars:300},ne=f.object({enabled:f.boolean().default(!0),isolated:f.boolean().default(!0),broadcastEvents:f.boolean().default(!1),storePath:f.string().default(""),heartbeat:ae.optional().default(oe)}),le={enabled:!0,isolated:!0,broadcastEvents:!1,storePath:"",heartbeat:oe},re=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)}),se={enabled:!0,port:3001,basePath:"/nostromo",configCheckInterval:5,autoRestart:!0},ie=f.object({gmabPath:f.string().optional().default("~/gmab"),host:f.string().optional().default("127.0.0.1"),logLevel:f.enum(["debug","info","warn","error"]).optional().default("info"),verboseDebugLogs:f.boolean().optional().default(!0),timezone:f.string().optional().default(""),fastProxyUrl:f.string().optional().default("http://localhost:4181"),channels:C.optional().default({telegram:{enabled:!1,accounts:{}},whatsapp:{enabled:!1,accounts:{}},discord:{enabled:!1,accounts:{}},slack:{enabled:!1,accounts:{}},signal:{enabled:!1,accounts:{}},msteams:{enabled:!1,accounts:{}},googlechat:{enabled:!1,accounts:{}},line:{enabled:!1,accounts:{}},matrix:{enabled:!1,accounts:{}},responses:{enabled:!0,port:3004}}),models:_.optional().default(O),stt:A.optional().default({enabled:!1,provider:"openai-whisper","openai-whisper":{modelRef:"",model:"whisper-1",language:""},"local-whisper":{binaryPath:"whisper",model:"base"}}),tts:z.optional().default(Y),memory:K.optional().default(ee),agent:J.optional().default(te),cron:ne.optional().default(le),nostromo:re.optional().default(se),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 de(e){if("string"==typeof e)return e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(Array.isArray(e))return e.map(de);if(null!==e&&"object"==typeof e){const t={};for(const[a,o]of Object.entries(e))t[a]=de(o);return t}return e}const ce={channels:{responses:{enabled:!0,port:3004}},stt:{enabled:!1},tts:Y,memory:ee,agent:{...te,permissionMode:"bypassPermissions",allowedTools:["Read","Grep","Bash","WebSearch","Glob","Write","Edit","WebFetch","Task","Skill"]},cron:le,nostromo:se};export function loadConfig(n){const l=n??r(process.cwd(),"config.yaml"),i=r(process.cwd(),".env");if(a(i)&&(u({path:i}),b.info(`Loaded .env from ${i}`)),!a(l)){const e="# GrabMeABeer Configuration\n# Configure channels and settings via Nostromo: http://localhost:3001\n\n"+c({gmabPath:"~/gmab",...ce});t(l,e,"utf-8")}const f=e(l,"utf-8");ue(f);const p=de(d(f)),m=ie.parse(p);if(h=process.env.GMAB_PATH?g(process.env.GMAB_PATH):g(m.gmabPath),y=s(h,"data"),o(y,{recursive:!0}),!m.timezone){m.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;try{const a=d(e(l,"utf-8"))??{};a.timezone=m.timezone,t(l,c(a),"utf-8"),b.info(`Timezone auto-detected and saved: ${m.timezone}`)}catch(e){}}return function(e){o(y,{recursive:!0});const t=process.env.WORKSPACE_PATH??e.agent.workspacePath;e.agent.workspacePath=r(g(t)),o(e.agent.workspacePath,{recursive:!0});const a=s(y,"cron");o(a,{recursive:!0});const n=e.cron.storePath.trim()?r(g(e.cron.storePath)):s(a,"jobs.json");return{...e,gmabPath:h,dataDir:y,dbPath:s(y,"core.db"),memoryDir:s(y,"memory"),cronStorePath:n}}(m)}export function loadRawConfig(t){const o=t??r(process.cwd(),"config.yaml");if(!a(o))return{};const n=e(o,"utf-8");return d(n)??{}}export function 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 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}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Hera Mesh Channel — inter-agent messaging via Hera Mesh broker.
3
+ *
4
+ * Each connected agent appears as a chatId. When Beatrice sends a message
5
+ * to Dante, it arrives as channel="mesh", chatId="beatrice".
6
+ * Dante can reply via sendText("beatrice", "...").
7
+ */
8
+ import type { ChannelAdapter, MessageHandler } from "../bridge.js";
9
+ export interface MeshChannelConfig {
10
+ brokerUrl: string;
11
+ agentId: string;
12
+ privateKey: string;
13
+ reconnectDelayMs?: number;
14
+ }
15
+ export declare class MeshChannel implements ChannelAdapter {
16
+ readonly name = "mesh";
17
+ private ws;
18
+ private config;
19
+ private onMessage;
20
+ private sessionToken;
21
+ private connected;
22
+ private reconnectTimer;
23
+ private reconnectDelay;
24
+ private stopping;
25
+ private pingTimer;
26
+ constructor(config: MeshChannelConfig);
27
+ start(onMessage: MessageHandler): Promise<void>;
28
+ private connect;
29
+ /**
30
+ * Convert an incoming mesh message into an IncomingMessage and dispatch it.
31
+ */
32
+ private handleMeshMessage;
33
+ sendText(chatId: string, text: string): Promise<void>;
34
+ /**
35
+ * Send a typed mesh message (for MCP tools).
36
+ */
37
+ sendTyped(to: string, type: string, payload: unknown, replyTo?: string): string;
38
+ getSessionToken(): string | null;
39
+ getBrokerHttpUrl(): string;
40
+ isConnected(): boolean;
41
+ private sendRaw;
42
+ private startPing;
43
+ private stopPing;
44
+ private scheduleReconnect;
45
+ stop(): Promise<void>;
46
+ }
47
+ //# sourceMappingURL=mesh.d.ts.map
@@ -0,0 +1 @@
1
+ import e from"node:crypto";import{WebSocket as t}from"ws";import{createLogger as s}from"../../utils/logger.js";const n=s("Mesh");function o(t,s){const n=function(t){const s=Buffer.from(t,"hex"),n=Buffer.from("302e020100300506032b657004220420","hex"),o=Buffer.concat([n,s]);return e.createPrivateKey({key:o,format:"der",type:"pkcs8"})}(s),o=Buffer.from(t,"hex");return e.sign(null,o,n).toString("hex")}export class MeshChannel{name="mesh";ws=null;config;onMessage=null;sessionToken=null;connected=!1;reconnectTimer=null;reconnectDelay;stopping=!1;pingTimer=null;constructor(e){this.config=e,this.reconnectDelay=e.reconnectDelayMs??5e3}async start(e){this.onMessage=e,this.stopping=!1,await this.connect()}connect(){return new Promise((e,s)=>{const{brokerUrl:i,agentId:r}=this.config;n.info(`Connecting to mesh broker at ${i} as ${r}...`);try{this.ws=new t(i)}catch(t){return n.error(`Failed to create WebSocket: ${t}`),this.scheduleReconnect(),void e()}let c=!1;const a=t=>{c||(c=!0,t?(n.error(`Mesh connection failed: ${t.message}`),this.scheduleReconnect(),e()):e())},h=setTimeout(()=>{a(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 n.warn("Invalid JSON from mesh broker")}switch(t.type){case"challenge":{const e=o(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,n.info(`Authenticated as ${this.config.agentId} on mesh`),this.startPing(),a()):a(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":n.info(`Mesh presence: ${t.agentId} is ${t.status}`);break;case"ping":this.sendRaw({type:"pong"});break;case"pong":case"ack":break;case"error":n.warn(`Mesh error: ${t.error}`)}}),this.ws.on("close",(e,t)=>{clearTimeout(h),this.connected=!1,this.sessionToken=null,this.stopPing(),n.info(`Mesh connection closed (code=${e}, reason=${t?.toString()??"?"})`),this.stopping||this.scheduleReconnect(),a()}),this.ws.on("error",e=>{n.error(`Mesh WebSocket error: ${e.message}`)})})}async handleMeshMessage(e){if(!this.onMessage)return;const t=e.from;let s;if("string"==typeof e.payload)s=e.payload;else if(e.payload&&"object"==typeof e.payload){const t=e.payload;s="string"==typeof t.text?t.text:`[Mesh ${e.type}] ${JSON.stringify(e.payload)}`}else s=`[Mesh ${e.type}]`;const o={chatId:t,userId:e.from,channelName:"mesh",text:s,attachments:[],username:e.from};n.info(`Message from ${e.from}: ${e.type} → dispatching to agent`);try{const e=await this.onMessage(o);e&&e.trim()&&await this.sendText(t,e)}catch(t){n.error(`Error handling mesh message from ${e.from}: ${t}`)}}async sendText(t,s){if(!this.connected||!this.ws)return void n.warn(`Cannot send to ${t}: mesh not connected`);const o={id:e.randomUUID(),from:this.config.agentId,to:t,type:"text",payload:{text:s},timestamp:Date.now()};this.sendRaw({type:"message",message:o}),n.debug(`Sent to ${t}: ${s.slice(0,80)}...`)}sendTyped(t,s,n,o){if(!this.connected||!this.ws)throw new Error("Mesh not connected");const i={id:e.randomUUID(),from:this.config.agentId,to:t,type:s,payload:n,timestamp:Date.now(),replyTo:o};return this.sendRaw({type:"message",message:i}),i.id}getSessionToken(){return this.sessionToken}getBrokerHttpUrl(){return this.config.brokerUrl.replace("ws://","http://").replace("wss://","https://").replace("/ws","")}isConnected(){return this.connected}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||(n.info(`Reconnecting in ${this.reconnectDelay}ms...`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect().catch(e=>{n.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,n.info("Mesh channel stopped")}}
@@ -1,11 +1,12 @@
1
1
  import type { MemoryProvider, ConversationMessage, MemorySearchResult } from "./memory-provider.js";
2
2
  export declare class MemoryManager implements MemoryProvider {
3
3
  private baseDir;
4
+ private timezone?;
4
5
  /** sessionKey → current stem (e.g. "2026-02-06-2324") */
5
6
  private currentStem;
6
7
  /** sessionKeys that have been cleared — next append creates a new file */
7
8
  private cleared;
8
- constructor(baseDir: string);
9
+ constructor(baseDir: string, timezone?: string | undefined);
9
10
  private getChatDir;
10
11
  /** Returns the stem for the current session (e.g. "2026-02-06-2324") */
11
12
  private getCurrentStem;
@@ -1 +1 @@
1
- import{mkdirSync as t,appendFileSync as e,readFileSync as n,writeFileSync as s,existsSync as r,readdirSync as i}from"node:fs";import{join as o}from"node:path";import{createLogger as a}from"../utils/logger.js";const c=a("MemoryManager");export class MemoryManager{baseDir;currentStem=new Map;cleared=new Set;constructor(e){this.baseDir=e,t(e,{recursive:!0})}getChatDir(e){const n=e.replace(/:/g,"_"),s=o(this.baseDir,n);return t(s,{recursive:!0}),s}getCurrentStem(t){const e=this.currentStem.get(t);if(e)return e;if(!this.cleared.has(t)){const e=this.getChatDir(t),n=i(e).filter(t=>t.endsWith(".md")).sort();if(n.length>0){const e=n[n.length-1].replace(".md","");return this.currentStem.set(t,e),e}}const n=function(){const t=new Date,e=t=>String(t).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())}-${e(t.getHours())}${e(t.getMinutes())}`}();return this.currentStem.set(t,n),this.cleared.delete(t),n}getConversationFile(t){const e=this.getCurrentStem(t);return o(this.getChatDir(t),`${e}.md`)}getAttachmentsDir(e){const n=this.getCurrentStem(e),s=o(this.getChatDir(e),n);return t(s,{recursive:!0}),s}ensureConversationFile(t){const n=this.getConversationFile(t);if(!r(n)){const s=[`# Memory: ${t}`,`- Started: ${(new Date).toISOString()}`,"","---",""].join("\n");e(n,s,"utf-8"),c.info(`Memory file created: ${n}`)}return n}async append(t,n,s,r){const i=this.ensureConversationFile(t);let o=`### ${n} (${(new Date).toISOString()})\n`;r&&r.length>0&&(o+=`[files: ${r.join(", ")}]\n`),o+=`\n${s}\n\n`,e(i,o,"utf-8")}async saveFile(t,e,n){const r=this.getAttachmentsDir(t),i=(new Date).toISOString().replace(/[-:]/g,"").replace("T","_").slice(0,15),a=n.replace(/[^a-zA-Z0-9._-]/g,"_").slice(0,100),l=o(r,`${i}_${a}`);return s(l,e),c.info(`File saved to memory: ${l} (${e.length} bytes)`),l}getConversationMessages(t){const e=this.getConversationFile(t);if(!r(e))return[];return l(n(e,"utf-8"))}retrieveFromMemory(t){const e=[];if(!r(this.baseDir))return e;const s=t.toLowerCase(),a=i(this.baseDir,{withFileTypes:!0});for(const t of a){if(!t.isDirectory())continue;const r=o(this.baseDir,t.name),a=i(r).filter(t=>t.endsWith(".md"));for(const i of a){const a=o(r,i),c=l(n(a,"utf-8")),h=[],u=new Set;let f="",g="";for(const t of c)if(t.content.toLowerCase().includes(s)){if(h.push(m(t.content,s)),!f&&t.timestamp){const e=new Date(t.timestamp);isNaN(e.getTime())||(f=e.toISOString().slice(0,10),g=e.toISOString().slice(11,19))}for(const e of t.attachments)u.add(e)}h.length>0&&e.push({memoryFile:a,sessionKey:t.name,date:f,time:g,snippets:h,files:Array.from(u)})}}return e}clearSession(t){this.currentStem.delete(t),this.cleared.add(t),c.info(`Memory session cleared for ${t} — next message starts a new file`)}}function l(t){const e=[],n=t.split(/^### /m).slice(1);for(const t of n){const n=t.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/);if(!n)continue;const s=n[1],r=n[2],i=t.slice(n[0].length);let o,a=[];const c=i.match(/^\[files:\s*(.+?)\]\s*\n/);c?(a=c[1].split(",").map(t=>t.trim()),o=i.slice(c[0].length).trim()):o=i.trim(),e.push({role:s,content:o,timestamp:r,attachments:a})}return e}function m(t,e,n=150){const s=t.toLowerCase().indexOf(e);if(-1===s)return t.slice(0,2*n);const r=Math.max(0,s-n),i=Math.min(t.length,s+e.length+n);let o=t.slice(r,i);return r>0&&(o="..."+o),i<t.length&&(o+="..."),o}
1
+ import{mkdirSync as t,appendFileSync as e,readFileSync as n,writeFileSync as i,existsSync as r,readdirSync as o}from"node:fs";import{join as s}from"node:path";import{createLogger as a}from"../utils/logger.js";const c=a("MemoryManager");export class MemoryManager{baseDir;timezone;currentStem=new Map;cleared=new Set;constructor(e,n){this.baseDir=e,this.timezone=n,t(e,{recursive:!0})}getChatDir(e){const n=e.replace(/:/g,"_"),i=s(this.baseDir,n);return t(i,{recursive:!0}),i}getCurrentStem(t){const e=this.currentStem.get(t);if(e)return e;if(!this.cleared.has(t)){const e=this.getChatDir(t),n=o(e).filter(t=>t.endsWith(".md")).sort();if(n.length>0){const e=n[n.length-1].replace(".md","");return this.currentStem.set(t,e),e}}const n=function(t){const e=new Date;if(t){return`${e.toLocaleString("en-US",{timeZone:t,year:"numeric"})}-${e.toLocaleString("en-US",{timeZone:t,month:"2-digit"})}-${e.toLocaleString("en-US",{timeZone:t,day:"2-digit"})}-${e.toLocaleString("en-GB",{timeZone:t,hour:"2-digit",hour12:!1})}${e.toLocaleString("en-GB",{timeZone:t,minute:"2-digit"}).padStart(2,"0")}`}const n=t=>String(t).padStart(2,"0");return`${e.getFullYear()}-${n(e.getMonth()+1)}-${n(e.getDate())}-${n(e.getHours())}${n(e.getMinutes())}`}(this.timezone);return this.currentStem.set(t,n),this.cleared.delete(t),n}getConversationFile(t){const e=this.getCurrentStem(t);return s(this.getChatDir(t),`${e}.md`)}getAttachmentsDir(e){const n=this.getCurrentStem(e),i=s(this.getChatDir(e),n);return t(i,{recursive:!0}),i}ensureConversationFile(t){const n=this.getConversationFile(t);if(!r(n)){const i=[`# Memory: ${t}`,`- Started: ${(new Date).toISOString()}`,"","---",""].join("\n");e(n,i,"utf-8"),c.info(`Memory file created: ${n}`)}return n}async append(t,n,i,r){const o=this.ensureConversationFile(t);let s=`### ${n} (${function(t,e){if(!e)return t.toISOString();const n=t.toLocaleString("en-US",{timeZone:e,year:"numeric"}),i=t.toLocaleString("en-US",{timeZone:e,month:"2-digit"}),r=t.toLocaleString("en-US",{timeZone:e,day:"2-digit"}),o=t.toLocaleString("en-GB",{timeZone:e,hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1});return`${n}-${i}-${r}T${o}`}(new Date,this.timezone)})\n`;r&&r.length>0&&(s+=`[files: ${r.join(", ")}]\n`),s+=`\n${i}\n\n`,e(o,s,"utf-8")}async saveFile(t,e,n){const r=this.getAttachmentsDir(t),o=(new Date).toISOString().replace(/[-:]/g,"").replace("T","_").slice(0,15),a=n.replace(/[^a-zA-Z0-9._-]/g,"_").slice(0,100),m=s(r,`${o}_${a}`);return i(m,e),c.info(`File saved to memory: ${m} (${e.length} bytes)`),m}getConversationMessages(t){const e=this.getConversationFile(t);if(!r(e))return[];return m(n(e,"utf-8"))}retrieveFromMemory(t){const e=[];if(!r(this.baseDir))return e;const i=t.toLowerCase(),a=o(this.baseDir,{withFileTypes:!0});for(const t of a){if(!t.isDirectory())continue;const r=s(this.baseDir,t.name),a=o(r).filter(t=>t.endsWith(".md"));for(const o of a){const a=s(r,o),c=m(n(a,"utf-8")),g=[],h=new Set;let u="",f="";for(const t of c)if(t.content.toLowerCase().includes(i)){if(g.push(l(t.content,i)),!u&&t.timestamp){const e=new Date(t.timestamp);isNaN(e.getTime())||(u=e.toISOString().slice(0,10),f=e.toISOString().slice(11,19))}for(const e of t.attachments)h.add(e)}g.length>0&&e.push({memoryFile:a,sessionKey:t.name,date:u,time:f,snippets:g,files:Array.from(h)})}}return e}clearSession(t){this.currentStem.delete(t),this.cleared.add(t),c.info(`Memory session cleared for ${t} — next message starts a new file`)}}function m(t){const e=[],n=t.split(/^### /m).slice(1);for(const t of n){const n=t.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/);if(!n)continue;const i=n[1],r=n[2],o=t.slice(n[0].length);let s,a=[];const c=o.match(/^\[files:\s*(.+?)\]\s*\n/);c?(a=c[1].split(",").map(t=>t.trim()),s=o.slice(c[0].length).trim()):s=o.trim(),e.push({role:i,content:s,timestamp:r,attachments:a})}return e}function l(t,e,n=150){const i=t.toLowerCase().indexOf(e);if(-1===i)return t.slice(0,2*n);const r=Math.max(0,i-n),o=Math.min(t.length,i+e.length+n);let s=t.slice(r,o);return r>0&&(s="..."+s),o<t.length&&(s+="..."),s}
@@ -97,6 +97,16 @@ export declare class MemorySearch {
97
97
  path: string;
98
98
  content: string;
99
99
  };
100
+ /**
101
+ * List memory files whose filename matches a YYYY-MM-DD date range.
102
+ * Files are matched by the date prefix in their basename (e.g. "2026-03-19-2038.md" matches "2026-03-19").
103
+ * Returns relative paths compatible with readFile / memory_get.
104
+ */
105
+ listFiles(from: string, to: string): Array<{
106
+ path: string;
107
+ sessionKey: string;
108
+ size: number;
109
+ }>;
100
110
  expandChunks(ids: number[]): Array<{
101
111
  chunkId: number;
102
112
  content: string;
@@ -1 +1 @@
1
- import{existsSync as e,readdirSync as t,readFileSync as n,statSync as s,copyFileSync as i,renameSync as r,unlinkSync as h,watch as a}from"node:fs";import{join as o,relative as c,basename as d,dirname as l}from"node:path";import m from"better-sqlite3";import u from"openai";import p from"hnswlib-node";const{HierarchicalNSW:E}=p;import{createLogger as b}from"../utils/logger.js";import{L0Generator as f,L0_DEFAULT as g}from"./l0-generator.js";const T=b("MemorySearch"),x="cosine";export class MemorySearch{memoryDir;dataDir;opts;indexDb=null;indexHnsw=null;watcher=null;debounceTimer=null;embedTimer=null;indexing=!1;embedding=!1;stopped=!0;l0Generator;searchDb=null;searchHnsw=null;openai=null;indexDbPath;searchDbPath;searchNextDbPath;indexHnswPath;searchHnswPath;searchNextHnswPath;constructor(e,t,n){this.memoryDir=e,this.dataDir=t,this.opts=n,this.indexDbPath=o(t,"memory-index.db"),this.searchDbPath=o(t,"memory-search.db"),this.searchNextDbPath=o(t,"memory-search-next.db"),this.indexHnswPath=o(t,"memory-vectors.hnsw"),this.searchHnswPath=o(t,"memory-vectors-search.hnsw"),this.searchNextHnswPath=o(t,"memory-vectors-search-next.hnsw"),this.isOpenAI()&&(this.openai=new u({apiKey:n.apiKey,...n.baseURL?{baseURL:n.baseURL}:{}})),this.l0Generator=new f(n.l0??g)}isOpenAI(){return(this.opts.baseURL||"https://api.openai.com/v1").includes("openai.com")}getMaxInjectedChars(){return this.opts.maxInjectedChars}async start(){T.info("Starting memory search engine..."),this.stopped=!1,this.indexDb=new m(this.indexDbPath),this.indexDb.pragma("journal_mode = WAL"),this.migrateEmbeddingsTable(),this.indexDb.exec("\n CREATE TABLE IF NOT EXISTS documents (\n path TEXT PRIMARY KEY,\n mtime_ms INTEGER NOT NULL,\n size INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n doc_path TEXT NOT NULL,\n chunk_idx INTEGER NOT NULL,\n role TEXT NOT NULL DEFAULT '',\n timestamp TEXT NOT NULL DEFAULT '',\n session_key TEXT NOT NULL DEFAULT '',\n content TEXT NOT NULL,\n UNIQUE(doc_path, chunk_idx)\n );\n\n CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n content,\n content='chunks',\n content_rowid='id',\n tokenize='porter unicode61'\n );\n\n CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n -- Lookup table only (no vector BLOB) — vectors live in HNSW index\n CREATE TABLE IF NOT EXISTS embeddings (\n chunk_id INTEGER PRIMARY KEY\n );\n\n -- Metadata for detecting config changes (model, dimensions)\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n -- Utility scoring: tracks how often each chunk is retrieved and used\n CREATE TABLE IF NOT EXISTS chunk_utility (\n chunk_id INTEGER PRIMARY KEY,\n access_count INTEGER DEFAULT 0,\n last_accessed TEXT,\n first_accessed TEXT\n );\n"),this.checkEmbeddingConfigChange(),this.initIndexHnsw(),await this.indexFiles(),await this.embedPending(),this.indexDb&&await this.l0Generator.start(this.indexDb),this.publishSnapshot(),this.maybeSwap(),this.startWatcher(),this.opts.embedIntervalMs>0&&(this.embedTimer=setInterval(()=>{this.embedPending().catch(e=>T.error(`Embed cycle error: ${e}`))},this.opts.embedIntervalMs)),T.info("Memory search engine started")}stop(){this.stopped=!0,this.watcher&&(this.watcher.close(),this.watcher=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.embedTimer&&(clearInterval(this.embedTimer),this.embedTimer=null),this.indexDb&&(this.indexDb.close(),this.indexDb=null),this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.indexHnsw=null,this.searchHnsw=null,this.l0Generator.stop(),T.info("Memory search engine stopped")}migrateEmbeddingsTable(){if(!this.indexDb)return;if(this.indexDb.prepare("PRAGMA table_info(embeddings)").all().some(e=>"vector"===e.name)){T.info("Migrating: dropping old embeddings table (had vector BLOB). All embeddings will be re-created via HNSW."),this.indexDb.exec("DROP TABLE IF EXISTS embeddings");try{h(this.indexHnswPath)}catch{}}}checkEmbeddingConfigChange(){if(!this.indexDb)return;const e=this.indexDb.prepare("SELECT value FROM meta WHERE key = ?"),t=this.indexDb.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),n=e.get("embedding_model")?.value,s=e.get("embedding_dimensions")?.value,i=this.opts.embeddingModel,r=String(this.opts.embeddingDimensions),a=void 0!==n&&n!==i,o=void 0!==s&&s!==r;if(a||o){const e=[];a&&e.push(`model: ${n} → ${i}`),o&&e.push(`dimensions: ${s} → ${r}`),T.info(`Embedding config changed (${e.join(", ")}). Wiping embeddings + HNSW for full re-embed.`),this.indexDb.exec("DELETE FROM embeddings");try{h(this.indexHnswPath)}catch{}}t.run("embedding_model",i),t.run("embedding_dimensions",r)}initIndexHnsw(){const t=this.opts.embeddingDimensions;if(this.indexHnsw=new E(x,t),e(this.indexHnswPath))try{this.indexHnsw.readIndexSync(this.indexHnswPath,!0),T.info(`Loaded HNSW index: ${this.indexHnsw.getCurrentCount()} points`)}catch(e){T.warn(`Failed to load HNSW index, creating new: ${e}`),this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0})}else this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0}),T.info("Created new HNSW index")}ensureHnswCapacity(e){if(!this.indexHnsw)return;const t=this.indexHnsw.getMaxElements();if(this.indexHnsw.getCurrentCount()+e>t){const n=Math.max(2*t,this.indexHnsw.getCurrentCount()+e+1e3);this.indexHnsw.resizeIndex(n),T.info(`Resized HNSW index: ${t} → ${n}`)}}async search(e,t){const n=t??this.opts.maxResults;if(this.maybeSwap(),!this.searchDb)return T.warn("Search DB not available"),[];const s=this.bm25Search(e,20);let i=[];try{const t=await this.embedText(e);t&&this.searchHnsw&&this.searchHnsw.getCurrentCount()>0&&(i=this.denseSearch(t,20))}catch(e){T.warn(`Dense search failed, using BM25 only: ${e}`)}const r=function(e,t,n){const s=new Map;for(let t=0;t<e.length;t++){const{id:i}=e[t];s.set(i,(s.get(i)??0)+1/(n+t+1))}for(let e=0;e<t.length;e++){const{id:i}=t[e];s.set(i,(s.get(i)??0)+1/(n+e+1))}const i=Array.from(s.entries()).map(([e,t])=>({id:e,score:t})).sort((e,t)=>t.score-e.score);return i}(s,i,this.opts.rrfK),h=Math.min(r.length,40),a=this.searchDb.prepare("SELECT doc_path, role, timestamp, session_key, content, l0 FROM chunks WHERE id = ?"),o=[];for(let e=0;e<h;e++){const{id:t,score:n}=r[e],s=a.get(t);s&&o.push({id:t,score:n,row:s})}const c=this.getUtilityScores(o.map(e=>e.id)),d=Date.now();for(const e of o){const t=c.get(e.id),n=t?.access_count??0;if(n>0&&(e.score+=.05*Math.log(n+1)),0===n&&e.row.timestamp){const t=new Date(e.row.timestamp).getTime();if(!isNaN(t)){const n=(d-t)/864e5;n>30&&(e.score-=Math.min(.001*(n-30),.05))}}}o.sort((e,t)=>t.score-e.score);const l=function(e,t,n){if(e.length<=t)return e;const s=e.map(e=>function(e){const t=new Set;for(const n of e.toLowerCase().matchAll(/\b\w{2,}\b/g))t.add(n[0]);return t}(e.row.content)),i=e[0]?.score??1,r=e[e.length-1]?.score??0,h=i-r||1,a=[],o=new Set(e.map((e,t)=>t));a.push(0),o.delete(0);for(;a.length<t&&o.size>0;){let t=-1,i=-1/0;for(const c of o){const o=(e[c].score-r)/h;let d=0;for(const e of a){const t=D(s[c],s[e]);t>d&&(d=t)}const l=n*o-(1-n)*d;l>i&&(i=l,t=c)}if(t<0)break;a.push(t),o.delete(t)}return a.map(t=>e[t])}(o,n,.7),m=[];for(const{id:e,score:t,row:n}of l){const s=n.l0&&this.l0Generator.enabled?n.l0:n.content.length>this.opts.maxSnippetChars?n.content.slice(0,this.opts.maxSnippetChars)+"...":n.content;m.push({chunkId:e,path:n.doc_path,sessionKey:n.session_key,snippet:s,score:t,role:n.role,timestamp:n.timestamp})}return T.info(`Search "${e.slice(0,60)}": ${m.length} results (sparse=${s.length}, dense=${i.length}, candidates=${o.length}, mmr=${l.length})`),m}recordAccess(e){if(!this.indexDb||0===e.length)return;const t=(new Date).toISOString(),n=this.indexDb.prepare("\n INSERT INTO chunk_utility (chunk_id, access_count, last_accessed, first_accessed)\n VALUES (?, 1, ?, ?)\n ON CONFLICT(chunk_id) DO UPDATE SET\n access_count = access_count + 1,\n last_accessed = excluded.last_accessed\n ");this.indexDb.transaction(()=>{for(const s of e)n.run(s,t,t)})(),T.debug(`Recorded access for ${e.length} chunks`)}getUtilityScores(e){const t=new Map;if(!this.searchDb||0===e.length)return t;if(!this.searchDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunk_utility'").get())return t;const n=e.map(()=>"?").join(","),s=this.searchDb.prepare(`SELECT chunk_id, access_count, last_accessed, first_accessed FROM chunk_utility WHERE chunk_id IN (${n})`).all(...e);for(const e of s)t.set(e.chunk_id,{access_count:e.access_count,last_accessed:e.last_accessed,first_accessed:e.first_accessed});return t}readFile(t,s,i){const r=o(this.memoryDir,t);if(!e(r))return{path:t,content:`[File not found: ${t}]`};const h=n(r,"utf-8"),a=h.split("\n");if(void 0!==s||void 0!==i){const e=Math.max(0,(s??1)-1),n=i??a.length;return{path:t,content:a.slice(e,e+n).join("\n")}}return{path:t,content:h}}expandChunks(e){if(!this.searchDb||0===e.length)return[];const t=e.map(()=>"?").join(","),n=this.searchDb.prepare(`SELECT id, content, doc_path, role, timestamp FROM chunks WHERE id IN (${t})`).all(...e);return n.length>0&&this.recordAccess(n.map(e=>e.id)),n.map(e=>({chunkId:e.id,content:e.content,path:e.doc_path,role:e.role,timestamp:e.timestamp}))}bm25Search(e,t){if(!this.searchDb)return[];try{const n=function(e){const t=e.replace(/[^\w\s]/g," ").split(/\s+/).filter(e=>e.length>0);return 0===t.length?"":t.map(e=>`"${e}"`).join(" OR ")}(e);if(!n)return[];return this.searchDb.prepare("\n SELECT chunks.id, bm25(chunks_fts) as rank\n FROM chunks_fts\n JOIN chunks ON chunks.id = chunks_fts.rowid\n WHERE chunks_fts MATCH ?\n ORDER BY rank\n LIMIT ?\n ").all(n,t).map(e=>({id:e.id,score:-e.rank}))}catch(e){return T.warn(`BM25 search error: ${e}`),[]}}denseSearch(e,t){if(!this.searchHnsw||0===this.searchHnsw.getCurrentCount())return[];const n=Math.min(t,this.searchHnsw.getCurrentCount()),s=this.searchHnsw.searchKnn(Array.from(e),n);return s.neighbors.map((e,t)=>({id:e,score:1-s.distances[t]}))}startWatcher(){if(e(this.memoryDir))try{this.watcher=a(this.memoryDir,{recursive:!0},(e,t)=>{this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{this.runIndexCycle()},this.opts.updateDebounceMs)})}catch(e){T.warn(`Could not start file watcher: ${e}`)}}async runIndexCycle(){if(!this.indexing){this.indexing=!0;try{await this.indexFiles(),this.indexDb&&await this.l0Generator.generate(this.indexDb),this.publishSnapshot()}catch(e){T.error(`Index cycle error: ${e}`)}finally{this.indexing=!1}}}async indexFiles(){if(!this.indexDb||!e(this.memoryDir))return;const i=function(e){const n=[];function i(r){let h;try{h=t(r,{withFileTypes:!0})}catch{return}for(const t of h){const h=o(r,t.name);if(t.isDirectory())i(h);else if(t.name.endsWith(".md"))try{const t=s(h),i=c(e,h),r=d(l(h));n.push({fullPath:h,relPath:i,sessionKey:r===d(e)?"":r,mtimeMs:Math.floor(t.mtimeMs),size:t.size})}catch{}}}return i(e),n}(this.memoryDir);let r=0;const h=this.indexDb.prepare("INSERT OR REPLACE INTO documents (path, mtime_ms, size) VALUES (?, ?, ?)"),a=this.indexDb.prepare("SELECT mtime_ms, size FROM documents WHERE path = ?"),m=this.indexDb.prepare("DELETE FROM chunks WHERE doc_path = ?"),u=this.indexDb.prepare("SELECT id FROM chunks WHERE doc_path = ?"),p=this.indexDb.prepare("DELETE FROM embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE doc_path = ?)"),E=this.indexDb.prepare("INSERT INTO chunks (doc_path, chunk_idx, role, timestamp, session_key, content) VALUES (?, ?, ?, ?, ?, ?)"),b=this.indexDb.prepare("SELECT path FROM documents").all().map(e=>e.path),f=new Set(i.map(e=>e.relPath));for(const e of b)if(!f.has(e)){const t=u.all(e);for(const{id:e}of t)try{this.indexHnsw?.markDelete(e)}catch{}p.run(e),m.run(e),this.indexDb.prepare("DELETE FROM documents WHERE path = ?").run(e),T.debug(`Removed deleted file from index: ${e}`)}for(const e of i){const t=a.get(e.relPath);if(t&&t.mtime_ms===e.mtimeMs&&t.size===e.size){r+=this.indexDb.prepare("SELECT COUNT(*) as c FROM chunks WHERE doc_path = ?").get(e.relPath).c;continue}const s=w(n(e.fullPath,"utf-8"),e.relPath,e.sessionKey),i=u.all(e.relPath);for(const{id:e}of i)try{this.indexHnsw?.markDelete(e)}catch{}p.run(e.relPath),m.run(e.relPath);this.indexDb.transaction(()=>{for(let t=0;t<s.length;t++){const n=s[t];E.run(e.relPath,t,n.role,n.timestamp,n.sessionKey,n.content)}h.run(e.relPath,e.mtimeMs,e.size)})(),r+=s.length,T.debug(`Indexed ${e.relPath}: ${s.length} chunks`)}T.info(`Indexed ${r} chunks from ${i.length} files`)}async embedPending(){if(!this.stopped&&!this.embedding&&this.indexDb&&this.indexHnsw){this.embedding=!0;try{const e=this.indexDb.prepare("\n SELECT c.id, c.content FROM chunks c\n LEFT JOIN embeddings e ON e.chunk_id = c.id\n WHERE e.chunk_id IS NULL\n ").all();if(0===e.length)return;T.info(`Embedding ${e.length} pending chunks...`),this.ensureHnswCapacity(e.length);const t=this.indexDb.prepare("INSERT OR REPLACE INTO embeddings (chunk_id) VALUES (?)");for(let n=0;n<e.length;n+=100){if(this.stopped)return void T.warn("embedPending aborted: engine stopped");const s=e.slice(n,n+100),i=s.map(e=>this.applyPrefix(this.opts.prefixDocument,e.content).slice(0,8e3)),r=this.opts.prefixDocument.trim();r&&T.debug(`Using prefixDocument (template: ${r}) → result sample: [${i[0].slice(0,80)}]`);try{let e;if(this.openai){const t=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:i,dimensions:this.opts.embeddingDimensions});e=t.data.sort((e,t)=>e.index-t.index).map(e=>e.embedding)}else e=await this.fetchEmbeddings(i);if(this.stopped||!this.indexDb||!this.indexHnsw)return void T.warn("embedPending aborted: engine stopped during embedding");this.indexDb.transaction(()=>{for(let n=0;n<e.length;n++)this.indexHnsw.addPoint(e[n],s[n].id,!0),t.run(s[n].id)})(),T.debug(`Embedded batch ${n/100+1}: ${s.length} chunks`)}catch(e){if(this.stopped)return;T.error(`Embedding batch failed: ${e}`)}}if(this.stopped||!this.indexHnsw)return;this.indexHnsw.writeIndexSync(this.indexHnswPath),this.publishSnapshot(),T.info(`Embedded ${e.length} chunks (HNSW: ${this.indexHnsw.getCurrentCount()} total points)`)}finally{this.embedding=!1}}}publishSnapshot(){if(!this.indexDb)return;const t=o(this.dataDir,".memory-search-next.tmp"),n=o(this.dataDir,".memory-vectors-search-next.tmp");try{this.indexDb.pragma("wal_checkpoint(TRUNCATE)"),i(this.indexDbPath,t),r(t,this.searchNextDbPath),e(this.indexHnswPath)&&(i(this.indexHnswPath,n),r(n,this.searchNextHnswPath)),T.debug("Published search snapshot (DB + HNSW)")}catch(e){T.error(`Failed to publish snapshot: ${e}`);try{h(t)}catch{}try{h(n)}catch{}}}maybeSwap(){if(e(this.searchNextDbPath))try{this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.searchHnsw=null,r(this.searchNextDbPath,this.searchDbPath),e(this.searchNextHnswPath)&&r(this.searchNextHnswPath,this.searchHnswPath),this.searchDb=new m(this.searchDbPath,{readonly:!0}),e(this.searchHnswPath)?(this.searchHnsw=new E(x,this.opts.embeddingDimensions),this.searchHnsw.readIndexSync(this.searchHnswPath),this.searchHnsw.setEf(50),T.debug(`Swapped to new search DB + HNSW (${this.searchHnsw.getCurrentCount()} points)`)):T.debug("Swapped to new search DB (no HNSW index yet)")}catch(t){T.error(`Failed to swap search DB: ${t}`);try{e(this.searchDbPath)&&(this.searchDb=new m(this.searchDbPath,{readonly:!0}))}catch{}}}applyPrefix(e,t){const n=e.trim();return n?n.replace(/\{content\}/g,()=>t):t}async fetchEmbeddings(e){const t=`${(this.opts.baseURL||"").replace(/\/+$/,"")}/embed`,n={"Content-Type":"application/json"};this.opts.apiKey&&(n.Authorization=`Bearer ${this.opts.apiKey}`);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({model:this.opts.embeddingModel,input:e})});if(!s.ok){const e=await s.text().catch(()=>"(no body)");throw new Error(`Embedding API ${s.status}: ${e.slice(0,300)}`)}const i=await s.json();if(Array.isArray(i.embeddings))return i.embeddings;throw new Error(`Unknown embedding response format. Keys: ${Object.keys(i).join(", ")}`)}async embedText(e){try{const t=this.applyPrefix(this.opts.prefixQuery,e),n=this.opts.prefixQuery.trim();if(n&&T.debug(`Using prefixQuery (template: ${n}) → result sample: [${t.slice(0,80)}]`),this.openai){const e=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:t.slice(0,8e3),dimensions:this.opts.embeddingDimensions});return new Float32Array(e.data[0].embedding)}const s=await this.fetchEmbeddings([t.slice(0,8e3)]);return new Float32Array(s[0])}catch(e){return T.error(`Failed to embed query: ${e}`),null}}}function w(e,t,n){const s=[],i=e.split(/^### /m);for(const e of i){if(!e.trim())continue;const t=e.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/),i=t?t[1]:"",r=t?t[2]:"",h=t?e.slice(t[0].length).trim():e.trim();if(!h)continue;const a=1500,o=100;if(h.length<=a)s.push({role:i,timestamp:r,sessionKey:n,content:h});else{let e=0;for(;e<h.length;){const t=Math.min(e+a,h.length),c=h.slice(e,t);if(s.push({role:i,timestamp:r,sessionKey:n,content:c}),e=t-o,e+o>=h.length)break}}}return s}function D(e,t){if(0===e.size&&0===t.size)return 0;let n=0;const s=e.size<=t.size?e:t,i=e.size<=t.size?t:e;for(const e of s)i.has(e)&&n++;const r=e.size+t.size-n;return 0===r?0:n/r}
1
+ import{existsSync as e,readdirSync as t,readFileSync as n,statSync as s,copyFileSync as i,renameSync as r,unlinkSync as h,watch as a}from"node:fs";import{join as o,relative as c,basename as d,dirname as l}from"node:path";import m from"better-sqlite3";import p from"openai";import u from"hnswlib-node";const{HierarchicalNSW:E}=u;import{createLogger as b}from"../utils/logger.js";import{L0Generator as f,L0_DEFAULT as g}from"./l0-generator.js";const T=b("MemorySearch"),x="cosine";export class MemorySearch{memoryDir;dataDir;opts;indexDb=null;indexHnsw=null;watcher=null;debounceTimer=null;embedTimer=null;indexing=!1;embedding=!1;stopped=!0;l0Generator;searchDb=null;searchHnsw=null;openai=null;indexDbPath;searchDbPath;searchNextDbPath;indexHnswPath;searchHnswPath;searchNextHnswPath;constructor(e,t,n){this.memoryDir=e,this.dataDir=t,this.opts=n,this.indexDbPath=o(t,"memory-index.db"),this.searchDbPath=o(t,"memory-search.db"),this.searchNextDbPath=o(t,"memory-search-next.db"),this.indexHnswPath=o(t,"memory-vectors.hnsw"),this.searchHnswPath=o(t,"memory-vectors-search.hnsw"),this.searchNextHnswPath=o(t,"memory-vectors-search-next.hnsw"),this.isOpenAI()&&(this.openai=new p({apiKey:n.apiKey,...n.baseURL?{baseURL:n.baseURL}:{}})),this.l0Generator=new f(n.l0??g)}isOpenAI(){return(this.opts.baseURL||"https://api.openai.com/v1").includes("openai.com")}getMaxInjectedChars(){return this.opts.maxInjectedChars}async start(){T.info("Starting memory search engine..."),this.stopped=!1,this.indexDb=new m(this.indexDbPath),this.indexDb.pragma("journal_mode = WAL"),this.migrateEmbeddingsTable(),this.indexDb.exec("\n CREATE TABLE IF NOT EXISTS documents (\n path TEXT PRIMARY KEY,\n mtime_ms INTEGER NOT NULL,\n size INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n doc_path TEXT NOT NULL,\n chunk_idx INTEGER NOT NULL,\n role TEXT NOT NULL DEFAULT '',\n timestamp TEXT NOT NULL DEFAULT '',\n session_key TEXT NOT NULL DEFAULT '',\n content TEXT NOT NULL,\n UNIQUE(doc_path, chunk_idx)\n );\n\n CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n content,\n content='chunks',\n content_rowid='id',\n tokenize='porter unicode61'\n );\n\n CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n END;\n\n CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES ('delete', old.id, old.content);\n INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\n END;\n\n -- Lookup table only (no vector BLOB) — vectors live in HNSW index\n CREATE TABLE IF NOT EXISTS embeddings (\n chunk_id INTEGER PRIMARY KEY\n );\n\n -- Metadata for detecting config changes (model, dimensions)\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n -- Utility scoring: tracks how often each chunk is retrieved and used\n CREATE TABLE IF NOT EXISTS chunk_utility (\n chunk_id INTEGER PRIMARY KEY,\n access_count INTEGER DEFAULT 0,\n last_accessed TEXT,\n first_accessed TEXT\n );\n"),this.checkEmbeddingConfigChange(),this.initIndexHnsw(),await this.indexFiles(),await this.embedPending(),this.indexDb&&await this.l0Generator.start(this.indexDb),this.publishSnapshot(),this.maybeSwap(),this.startWatcher(),this.opts.embedIntervalMs>0&&(this.embedTimer=setInterval(()=>{this.embedPending().catch(e=>T.error(`Embed cycle error: ${e}`))},this.opts.embedIntervalMs)),T.info("Memory search engine started")}stop(){this.stopped=!0,this.watcher&&(this.watcher.close(),this.watcher=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.embedTimer&&(clearInterval(this.embedTimer),this.embedTimer=null),this.indexDb&&(this.indexDb.close(),this.indexDb=null),this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.indexHnsw=null,this.searchHnsw=null,this.l0Generator.stop(),T.info("Memory search engine stopped")}migrateEmbeddingsTable(){if(!this.indexDb)return;if(this.indexDb.prepare("PRAGMA table_info(embeddings)").all().some(e=>"vector"===e.name)){T.info("Migrating: dropping old embeddings table (had vector BLOB). All embeddings will be re-created via HNSW."),this.indexDb.exec("DROP TABLE IF EXISTS embeddings");try{h(this.indexHnswPath)}catch{}}}checkEmbeddingConfigChange(){if(!this.indexDb)return;const e=this.indexDb.prepare("SELECT value FROM meta WHERE key = ?"),t=this.indexDb.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),n=e.get("embedding_model")?.value,s=e.get("embedding_dimensions")?.value,i=this.opts.embeddingModel,r=String(this.opts.embeddingDimensions),a=void 0!==n&&n!==i,o=void 0!==s&&s!==r;if(a||o){const e=[];a&&e.push(`model: ${n} → ${i}`),o&&e.push(`dimensions: ${s} → ${r}`),T.info(`Embedding config changed (${e.join(", ")}). Wiping embeddings + HNSW for full re-embed.`),this.indexDb.exec("DELETE FROM embeddings");try{h(this.indexHnswPath)}catch{}}t.run("embedding_model",i),t.run("embedding_dimensions",r)}initIndexHnsw(){const t=this.opts.embeddingDimensions;if(this.indexHnsw=new E(x,t),e(this.indexHnswPath))try{this.indexHnsw.readIndexSync(this.indexHnswPath,!0),T.info(`Loaded HNSW index: ${this.indexHnsw.getCurrentCount()} points`)}catch(e){T.warn(`Failed to load HNSW index, creating new: ${e}`),this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0})}else this.indexHnsw.initIndex({maxElements:1e4,m:16,efConstruction:200,allowReplaceDeleted:!0}),T.info("Created new HNSW index")}ensureHnswCapacity(e){if(!this.indexHnsw)return;const t=this.indexHnsw.getMaxElements();if(this.indexHnsw.getCurrentCount()+e>t){const n=Math.max(2*t,this.indexHnsw.getCurrentCount()+e+1e3);this.indexHnsw.resizeIndex(n),T.info(`Resized HNSW index: ${t} → ${n}`)}}async search(e,t){const n=t??this.opts.maxResults;if(this.maybeSwap(),!this.searchDb)return T.warn("Search DB not available"),[];const s=this.bm25Search(e,20);let i=[];try{const t=await this.embedText(e);t&&this.searchHnsw&&this.searchHnsw.getCurrentCount()>0&&(i=this.denseSearch(t,20))}catch(e){T.warn(`Dense search failed, using BM25 only: ${e}`)}const r=function(e,t,n){const s=new Map;for(let t=0;t<e.length;t++){const{id:i}=e[t];s.set(i,(s.get(i)??0)+1/(n+t+1))}for(let e=0;e<t.length;e++){const{id:i}=t[e];s.set(i,(s.get(i)??0)+1/(n+e+1))}const i=Array.from(s.entries()).map(([e,t])=>({id:e,score:t})).sort((e,t)=>t.score-e.score);return i}(s,i,this.opts.rrfK),h=Math.min(r.length,40),a=this.searchDb.prepare("SELECT doc_path, role, timestamp, session_key, content, l0 FROM chunks WHERE id = ?"),o=[];for(let e=0;e<h;e++){const{id:t,score:n}=r[e],s=a.get(t);s&&o.push({id:t,score:n,row:s})}const c=this.getUtilityScores(o.map(e=>e.id)),d=Date.now();for(const e of o){const t=c.get(e.id),n=t?.access_count??0;if(n>0&&(e.score+=.05*Math.log(n+1)),0===n&&e.row.timestamp){const t=new Date(e.row.timestamp).getTime();if(!isNaN(t)){const n=(d-t)/864e5;n>30&&(e.score-=Math.min(.001*(n-30),.05))}}}o.sort((e,t)=>t.score-e.score);const l=function(e,t,n){if(e.length<=t)return e;const s=e.map(e=>function(e){const t=new Set;for(const n of e.toLowerCase().matchAll(/\b\w{2,}\b/g))t.add(n[0]);return t}(e.row.content)),i=e[0]?.score??1,r=e[e.length-1]?.score??0,h=i-r||1,a=[],o=new Set(e.map((e,t)=>t));a.push(0),o.delete(0);for(;a.length<t&&o.size>0;){let t=-1,i=-1/0;for(const c of o){const o=(e[c].score-r)/h;let d=0;for(const e of a){const t=y(s[c],s[e]);t>d&&(d=t)}const l=n*o-(1-n)*d;l>i&&(i=l,t=c)}if(t<0)break;a.push(t),o.delete(t)}return a.map(t=>e[t])}(o,n,.7),m=[];for(const{id:e,score:t,row:n}of l){const s=n.l0&&this.l0Generator.enabled?n.l0:n.content.length>this.opts.maxSnippetChars?n.content.slice(0,this.opts.maxSnippetChars)+"...":n.content;m.push({chunkId:e,path:n.doc_path,sessionKey:n.session_key,snippet:s,score:t,role:n.role,timestamp:n.timestamp})}return T.info(`Search "${e.slice(0,60)}": ${m.length} results (sparse=${s.length}, dense=${i.length}, candidates=${o.length}, mmr=${l.length})`),m}recordAccess(e){if(!this.indexDb||0===e.length)return;const t=(new Date).toISOString(),n=this.indexDb.prepare("\n INSERT INTO chunk_utility (chunk_id, access_count, last_accessed, first_accessed)\n VALUES (?, 1, ?, ?)\n ON CONFLICT(chunk_id) DO UPDATE SET\n access_count = access_count + 1,\n last_accessed = excluded.last_accessed\n ");this.indexDb.transaction(()=>{for(const s of e)n.run(s,t,t)})(),T.debug(`Recorded access for ${e.length} chunks`)}getUtilityScores(e){const t=new Map;if(!this.searchDb||0===e.length)return t;if(!this.searchDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunk_utility'").get())return t;const n=e.map(()=>"?").join(","),s=this.searchDb.prepare(`SELECT chunk_id, access_count, last_accessed, first_accessed FROM chunk_utility WHERE chunk_id IN (${n})`).all(...e);for(const e of s)t.set(e.chunk_id,{access_count:e.access_count,last_accessed:e.last_accessed,first_accessed:e.first_accessed});return t}readFile(t,s,i){const r=o(this.memoryDir,t);if(!e(r))return{path:t,content:`[File not found: ${t}]`};const h=n(r,"utf-8"),a=h.split("\n");if(void 0!==s||void 0!==i){const e=Math.max(0,(s??1)-1),n=i??a.length;return{path:t,content:a.slice(e,e+n).join("\n")}}return{path:t,content:h}}listFiles(e,t){const n=D(this.memoryDir),s=[];for(const i of n){const n=d(i.relPath).match(/^(\d{4}-\d{2}-\d{2})/);if(!n)continue;const r=n[1];r>=e&&r<=t&&s.push({path:i.relPath,sessionKey:i.sessionKey,size:i.size})}return s.sort((e,t)=>e.path.localeCompare(t.path)),s}expandChunks(e){if(!this.searchDb||0===e.length)return[];const t=e.map(()=>"?").join(","),n=this.searchDb.prepare(`SELECT id, content, doc_path, role, timestamp FROM chunks WHERE id IN (${t})`).all(...e);return n.length>0&&this.recordAccess(n.map(e=>e.id)),n.map(e=>({chunkId:e.id,content:e.content,path:e.doc_path,role:e.role,timestamp:e.timestamp}))}bm25Search(e,t){if(!this.searchDb)return[];try{const n=function(e){const t=e.replace(/[^\w\s]/g," ").split(/\s+/).filter(e=>e.length>0);return 0===t.length?"":t.map(e=>`"${e}"`).join(" OR ")}(e);if(!n)return[];return this.searchDb.prepare("\n SELECT chunks.id, bm25(chunks_fts) as rank\n FROM chunks_fts\n JOIN chunks ON chunks.id = chunks_fts.rowid\n WHERE chunks_fts MATCH ?\n ORDER BY rank\n LIMIT ?\n ").all(n,t).map(e=>({id:e.id,score:-e.rank}))}catch(e){return T.warn(`BM25 search error: ${e}`),[]}}denseSearch(e,t){if(!this.searchHnsw||0===this.searchHnsw.getCurrentCount())return[];const n=Math.min(t,this.searchHnsw.getCurrentCount()),s=this.searchHnsw.searchKnn(Array.from(e),n);return s.neighbors.map((e,t)=>({id:e,score:1-s.distances[t]}))}startWatcher(){if(e(this.memoryDir))try{this.watcher=a(this.memoryDir,{recursive:!0},(e,t)=>{this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{this.runIndexCycle()},this.opts.updateDebounceMs)})}catch(e){T.warn(`Could not start file watcher: ${e}`)}}async runIndexCycle(){if(!this.indexing){this.indexing=!0;try{await this.indexFiles(),this.indexDb&&await this.l0Generator.generate(this.indexDb),this.publishSnapshot()}catch(e){T.error(`Index cycle error: ${e}`)}finally{this.indexing=!1}}}async indexFiles(){if(!this.indexDb||!e(this.memoryDir))return;const t=D(this.memoryDir);let s=0;const i=this.indexDb.prepare("INSERT OR REPLACE INTO documents (path, mtime_ms, size) VALUES (?, ?, ?)"),r=this.indexDb.prepare("SELECT mtime_ms, size FROM documents WHERE path = ?"),h=this.indexDb.prepare("DELETE FROM chunks WHERE doc_path = ?"),a=this.indexDb.prepare("SELECT id FROM chunks WHERE doc_path = ?"),o=this.indexDb.prepare("DELETE FROM embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE doc_path = ?)"),c=this.indexDb.prepare("INSERT INTO chunks (doc_path, chunk_idx, role, timestamp, session_key, content) VALUES (?, ?, ?, ?, ?, ?)"),d=this.indexDb.prepare("SELECT path FROM documents").all().map(e=>e.path),l=new Set(t.map(e=>e.relPath));for(const e of d)if(!l.has(e)){const t=a.all(e);for(const{id:e}of t)try{this.indexHnsw?.markDelete(e)}catch{}o.run(e),h.run(e),this.indexDb.prepare("DELETE FROM documents WHERE path = ?").run(e),T.debug(`Removed deleted file from index: ${e}`)}for(const e of t){const t=r.get(e.relPath);if(t&&t.mtime_ms===e.mtimeMs&&t.size===e.size){s+=this.indexDb.prepare("SELECT COUNT(*) as c FROM chunks WHERE doc_path = ?").get(e.relPath).c;continue}const d=w(n(e.fullPath,"utf-8"),e.relPath,e.sessionKey),l=a.all(e.relPath);for(const{id:e}of l)try{this.indexHnsw?.markDelete(e)}catch{}o.run(e.relPath),h.run(e.relPath);this.indexDb.transaction(()=>{for(let t=0;t<d.length;t++){const n=d[t];c.run(e.relPath,t,n.role,n.timestamp,n.sessionKey,n.content)}i.run(e.relPath,e.mtimeMs,e.size)})(),s+=d.length,T.debug(`Indexed ${e.relPath}: ${d.length} chunks`)}T.info(`Indexed ${s} chunks from ${t.length} files`)}async embedPending(){if(!this.stopped&&!this.embedding&&this.indexDb&&this.indexHnsw){this.embedding=!0;try{const e=this.indexDb.prepare("\n SELECT c.id, c.content FROM chunks c\n LEFT JOIN embeddings e ON e.chunk_id = c.id\n WHERE e.chunk_id IS NULL\n ").all();if(0===e.length)return;T.info(`Embedding ${e.length} pending chunks...`),this.ensureHnswCapacity(e.length);const t=this.indexDb.prepare("INSERT OR REPLACE INTO embeddings (chunk_id) VALUES (?)");for(let n=0;n<e.length;n+=100){if(this.stopped)return void T.warn("embedPending aborted: engine stopped");const s=e.slice(n,n+100),i=s.map(e=>this.applyPrefix(this.opts.prefixDocument,e.content).slice(0,8e3)),r=this.opts.prefixDocument.trim();r&&T.debug(`Using prefixDocument (template: ${r}) → result sample: [${i[0].slice(0,80)}]`);try{let e;if(this.openai){const t=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:i,dimensions:this.opts.embeddingDimensions});e=t.data.sort((e,t)=>e.index-t.index).map(e=>e.embedding)}else e=await this.fetchEmbeddings(i);if(this.stopped||!this.indexDb||!this.indexHnsw)return void T.warn("embedPending aborted: engine stopped during embedding");this.indexDb.transaction(()=>{for(let n=0;n<e.length;n++)this.indexHnsw.addPoint(e[n],s[n].id,!0),t.run(s[n].id)})(),T.debug(`Embedded batch ${n/100+1}: ${s.length} chunks`)}catch(e){if(this.stopped)return;T.error(`Embedding batch failed: ${e}`)}}if(this.stopped||!this.indexHnsw)return;this.indexHnsw.writeIndexSync(this.indexHnswPath),this.publishSnapshot(),T.info(`Embedded ${e.length} chunks (HNSW: ${this.indexHnsw.getCurrentCount()} total points)`)}finally{this.embedding=!1}}}publishSnapshot(){if(!this.indexDb)return;const t=o(this.dataDir,".memory-search-next.tmp"),n=o(this.dataDir,".memory-vectors-search-next.tmp");try{this.indexDb.pragma("wal_checkpoint(TRUNCATE)"),i(this.indexDbPath,t),r(t,this.searchNextDbPath),e(this.indexHnswPath)&&(i(this.indexHnswPath,n),r(n,this.searchNextHnswPath)),T.debug("Published search snapshot (DB + HNSW)")}catch(e){T.error(`Failed to publish snapshot: ${e}`);try{h(t)}catch{}try{h(n)}catch{}}}maybeSwap(){if(e(this.searchNextDbPath))try{this.searchDb&&(this.searchDb.close(),this.searchDb=null),this.searchHnsw=null,r(this.searchNextDbPath,this.searchDbPath),e(this.searchNextHnswPath)&&r(this.searchNextHnswPath,this.searchHnswPath),this.searchDb=new m(this.searchDbPath,{readonly:!0}),e(this.searchHnswPath)?(this.searchHnsw=new E(x,this.opts.embeddingDimensions),this.searchHnsw.readIndexSync(this.searchHnswPath),this.searchHnsw.setEf(50),T.debug(`Swapped to new search DB + HNSW (${this.searchHnsw.getCurrentCount()} points)`)):T.debug("Swapped to new search DB (no HNSW index yet)")}catch(t){T.error(`Failed to swap search DB: ${t}`);try{e(this.searchDbPath)&&(this.searchDb=new m(this.searchDbPath,{readonly:!0}))}catch{}}}applyPrefix(e,t){const n=e.trim();return n?n.replace(/\{content\}/g,()=>t):t}async fetchEmbeddings(e){const t=`${(this.opts.baseURL||"").replace(/\/+$/,"")}/embed`,n={"Content-Type":"application/json"};this.opts.apiKey&&(n.Authorization=`Bearer ${this.opts.apiKey}`);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({model:this.opts.embeddingModel,input:e})});if(!s.ok){const e=await s.text().catch(()=>"(no body)");throw new Error(`Embedding API ${s.status}: ${e.slice(0,300)}`)}const i=await s.json();if(Array.isArray(i.embeddings))return i.embeddings;throw new Error(`Unknown embedding response format. Keys: ${Object.keys(i).join(", ")}`)}async embedText(e){try{const t=this.applyPrefix(this.opts.prefixQuery,e),n=this.opts.prefixQuery.trim();if(n&&T.debug(`Using prefixQuery (template: ${n}) → result sample: [${t.slice(0,80)}]`),this.openai){const e=await this.openai.embeddings.create({model:this.opts.embeddingModel,input:t.slice(0,8e3),dimensions:this.opts.embeddingDimensions});return new Float32Array(e.data[0].embedding)}const s=await this.fetchEmbeddings([t.slice(0,8e3)]);return new Float32Array(s[0])}catch(e){return T.error(`Failed to embed query: ${e}`),null}}}function w(e,t,n){const s=[],i=e.split(/^### /m);for(const e of i){if(!e.trim())continue;const t=e.match(/^(user|assistant)\s*\(([^)]+)\)\s*\n/),i=t?t[1]:"",r=t?t[2]:"",h=t?e.slice(t[0].length).trim():e.trim();if(!h)continue;const a=1500,o=100;if(h.length<=a)s.push({role:i,timestamp:r,sessionKey:n,content:h});else{let e=0;for(;e<h.length;){const t=Math.min(e+a,h.length),c=h.slice(e,t);if(s.push({role:i,timestamp:r,sessionKey:n,content:c}),e=t-o,e+o>=h.length)break}}}return s}function D(e){const n=[];return function i(r){let h;try{h=t(r,{withFileTypes:!0})}catch{return}for(const t of h){const h=o(r,t.name);if(t.isDirectory())i(h);else if(t.name.endsWith(".md"))try{const t=s(h),i=c(e,h),r=d(l(h));n.push({fullPath:h,relPath:i,sessionKey:r===d(e)?"":r,mtimeMs:Math.floor(t.mtimeMs),size:t.size})}catch{}}}(e),n}function y(e,t){if(0===e.size&&0===t.size)return 0;let n=0;const s=e.size<=t.size?e:t,i=e.size<=t.size?t:e;for(const e of s)i.has(e)&&n++;const r=e.size+t.size-n;return 0===r?0:n/r}
@@ -1 +1 @@
1
- export function channelsJS(){return"\n/* ---- Channels ---- */\nconst CHANNEL_LIST = ['telegram','responses','whatsapp','discord','slack','signal','msteams','googlechat','line','matrix'];\nconst SUPPORTED_CHANNELS = ['telegram','responses','whatsapp'];\n\nconst CHANNEL_HELP = {\n telegram: {\n title: 'Telegram Setup',\n steps: [\n 'Open Telegram and message <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>',\n 'Send <code>/newbot</code>',\n 'Choose a display name, then a username (must end in <code>bot</code>)',\n 'BotFather replies with a <b>bot token</b> &mdash; paste it below',\n 'Optional: send <code>/setprivacy</code> &rarr; Disable, so the bot can see all group messages',\n '<b>DM Policy</b> controls who can talk to the bot: <b>open</b> = anyone can message, <b>allowlist</b> = only Telegram user IDs listed in Allow From can message',\n ]\n },\n discord: {\n title: 'Discord Setup',\n steps: [\n 'Go to <a href=\"https://discord.com/developers/applications\" target=\"_blank\">Discord Developer Portal</a> &rarr; <b>New Application</b>',\n '<b>Bot</b> tab &rarr; click <b>Reset Token</b> &rarr; copy the token and paste it below',\n 'On the same page, enable <b>Message Content Intent</b> under Privileged Gateway Intents',\n '<b>OAuth2 &rarr; URL Generator</b> &rarr; scope: <code>bot</code>, permission: <code>Send Messages</code>',\n 'Open the generated URL to invite the bot to your server',\n ]\n },\n slack: {\n title: 'Slack Setup',\n steps: [\n 'Go to <a href=\"https://api.slack.com/apps\" target=\"_blank\">api.slack.com/apps</a> &rarr; <b>Create New App</b> &rarr; From scratch',\n '<b>Socket Mode</b> &rarr; enable &rarr; generate an App-Level Token with scope <code>connections:write</code> &rarr; copy it (this is the <b>App Token</b>, starts with <code>xapp-</code>)',\n '<b>OAuth &amp; Permissions</b> &rarr; add Bot Token Scopes: <code>chat:write</code>, <code>im:history</code>, <code>im:read</code>',\n '<b>Event Subscriptions</b> &rarr; enable &rarr; subscribe to bot event <code>message.im</code>',\n 'Install the app to your workspace &rarr; copy the <b>Bot Token</b> (starts with <code>xoxb-</code>)',\n ]\n },\n whatsapp: {\n title: 'WhatsApp Setup',\n steps: [\n 'Set the <b>Auth Directory</b> below to a local folder (e.g. <code>./data/whatsapp</code>)',\n 'Enable the channel, save, then click <b>Connect WhatsApp</b> &mdash; a QR code will appear in a pop-up',\n 'Open WhatsApp on your phone &rarr; <b>Settings</b> &rarr; <b>Linked Devices</b> &rarr; scan the QR code',\n 'Once scanned, the pop-up confirms the connection. The session persists in the auth directory',\n '<b>DM Policy</b>: <b>open</b> = anyone can message, <b>allowlist</b> = only the phone numbers listed in Allow From can message (E.164 format, e.g. <code>+393331234567</code>)',\n ]\n },\n signal: {\n title: 'Signal Setup',\n steps: [\n 'Install <a href=\"https://github.com/bbernhard/signal-cli-rest-api\" target=\"_blank\">signal-cli-rest-api</a> and start it',\n 'Register or link a phone number via the signal-cli REST API',\n 'Enter the <b>API URL</b> (e.g. <code>http://localhost:8080</code>) and <b>Phone Number</b> below',\n ]\n },\n msteams: {\n title: 'Microsoft Teams Setup',\n steps: [\n 'Go to <a href=\"https://dev.teams.microsoft.com/\" target=\"_blank\">Teams Developer Portal</a> &rarr; <b>Apps</b> &rarr; New App',\n 'Under <b>App Features</b> &rarr; add a <b>Bot</b>',\n 'In Azure, register a new <b>Bot Channel Registration</b> &rarr; copy <b>App ID</b> and <b>App Secret</b>',\n 'Set the messaging endpoint to your server URL',\n 'Paste App ID and App Secret below',\n ]\n },\n googlechat: {\n title: 'Google Chat Setup',\n steps: [\n 'Go to <a href=\"https://console.cloud.google.com/\" target=\"_blank\">Google Cloud Console</a> &rarr; create or select a project',\n 'Enable the <b>Google Chat API</b>',\n '<b>Configuration</b> tab &rarr; set Bot URL to your server endpoint',\n 'Create a <b>Service Account</b> &rarr; download the JSON key file',\n 'Enter the path to the <b>credentials file</b> and <b>space ID</b> below',\n ]\n },\n line: {\n title: 'LINE Setup',\n steps: [\n 'Go to <a href=\"https://developers.line.biz/console/\" target=\"_blank\">LINE Developers Console</a> &rarr; create a Provider &rarr; create a <b>Messaging API</b> channel',\n 'Under <b>Messaging API</b> tab, issue a <b>Channel Access Token</b> &rarr; paste it below',\n 'Copy the <b>Channel Secret</b> from the <b>Basic Settings</b> tab',\n 'Set the <b>Webhook URL</b> to your server endpoint and enable <b>Use Webhook</b>',\n ]\n },\n matrix: {\n title: 'Matrix Setup',\n steps: [\n 'Create a bot account on your Matrix homeserver (e.g. via <code>register_new_matrix_user</code>)',\n 'Enter the <b>Homeserver URL</b> (e.g. <code>https://matrix.example.com</code>)',\n 'Enter the bot <b>User ID</b> (e.g. <code>@bot:example.com</code>) and <b>Access Token</b>',\n 'Invite the bot to the rooms you want it to participate in',\n ]\n },\n responses: {\n title: 'Responses API',\n steps: [\n 'This is a built-in HTTP API compatible with the OpenAI Responses format',\n 'Create an API token in the <b>Tokens</b> section to authenticate requests',\n 'Send requests to <code>POST http://&lt;host&gt;:&lt;port&gt;/v1/responses</code> with <code>Authorization: Bearer &lt;token&gt;</code>',\n ]\n },\n};\n\nfunction channelFields(ch, cfg) {\n const accts = cfg.accounts || {};\n const dfl = Object.values(accts)[0] || {};\n const aid = Object.keys(accts)[0] || 'default';\n let h = '';\n switch (ch) {\n case 'telegram':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n var dp = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dp==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dp==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(Telegram user IDs, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n break;\n case 'discord':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.token\" value=\"'+esc(dfl.token||'')+'\"></div>';\n break;\n case 'slack':\n h += '<div class=\"field\"><label>Bot Token <span style=\"color:var(--text-muted);font-weight:400\">(xoxb-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Token <span style=\"color:var(--text-muted);font-weight:400\">(xapp-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appToken\" value=\"'+esc(dfl.appToken||'')+'\"></div>';\n break;\n case 'whatsapp':\n h += '<div class=\"field\"><label>Auth Directory</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.authDir\" value=\"'+esc(dfl.authDir||'./data/whatsapp')+'\"></div>';\n var dpw = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dpw==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dpw==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(phone numbers in E.164 format, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n h += '<div class=\"field\"><button type=\"button\" class=\"btn btn-sm\" onclick=\"openWhatsAppQr()\" id=\"wa-connect-btn\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px\"><path d=\"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4\"/><polyline points=\"10 17 15 12 10 7\"/><line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\"/></svg>Connect WhatsApp</button></div>';\n break;\n case 'signal':\n h += '<div class=\"field\"><label>API URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.apiUrl\" value=\"'+esc(dfl.apiUrl||'http://localhost:8080')+'\"></div>';\n h += '<div class=\"field\"><label>Phone Number</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.phoneNumber\" value=\"'+esc(dfl.phoneNumber||'')+'\"></div>';\n break;\n case 'msteams':\n h += '<div class=\"field\"><label>App ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appId\" value=\"'+esc(dfl.appId||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Secret</label><input type=\"password\" data-ch-field=\"'+ch+'.'+aid+'.appSecret\" value=\"'+esc(dfl.appSecret||'')+'\"></div>';\n break;\n case 'googlechat':\n h += '<div class=\"field\"><label>Credentials File</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.credentialsFile\" value=\"'+esc(dfl.credentialsFile||'')+'\"></div>';\n h += '<div class=\"field\"><label>Space ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.spaceId\" value=\"'+esc(dfl.spaceId||'')+'\"></div>';\n break;\n case 'line':\n h += '<div class=\"field\"><label>Channel Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelAccessToken\" value=\"'+esc(dfl.channelAccessToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>Channel Secret</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelSecret\" value=\"'+esc(dfl.channelSecret||'')+'\"></div>';\n break;\n case 'matrix':\n h += '<div class=\"field\"><label>Homeserver URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.homeserverUrl\" value=\"'+esc(dfl.homeserverUrl||'')+'\"></div>';\n h += '<div class=\"field\"><label>User ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.userId\" value=\"'+esc(dfl.userId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.accessToken\" value=\"'+esc(dfl.accessToken||'')+'\"></div>';\n break;\n case 'responses':\n h += '<div class=\"field\"><label>Port</label><input type=\"number\" data-ch-field=\"'+ch+'.port\" value=\"'+(cfg.port||3000)+'\"></div>';\n break;\n }\n return h;\n}\n\nasync function loadChannels(){\n currentConfig = await fetchAPI('/config');\n const wrap = document.getElementById('channelCards');\n wrap.innerHTML='';\n for(const ch of CHANNEL_LIST){\n const cfg = currentConfig.channels?.[ch]||{enabled:false};\n const help = CHANNEL_HELP[ch];\n const helpId = 'help-'+ch;\n const supported = SUPPORTED_CHANNELS.indexOf(ch) !== -1;\n if(!supported){ continue; }\n let html = '<div class=\"card\" data-channel=\"'+ch+'\">';\n html += '<div class=\"card-header\"><span class=\"card-title\" style=\"text-transform:capitalize\">'+esc(ch)+'</span>';\n html += '<label class=\"toggle\"><input type=\"checkbox\" data-ch-toggle=\"'+ch+'\" '+(cfg.enabled?'checked':'')+'><span></span></label></div>';\n html += '<div class=\"ch-fields\" data-ch-fields=\"'+ch+'\" style=\"'+(cfg.enabled?'':'display:none')+'\">';\n if(help){\n html += '<div style=\"margin-bottom:10px\"><button class=\"help-toggle\" type=\"button\" onclick=\"toggleHelp(this)\" data-help=\"'+helpId+'\" title=\"Setup guide\">?</button> <span style=\"font-size:13px;color:var(--text-muted)\">How to set up</span></div>';\n html += '<div class=\"help-panel\" id=\"'+helpId+'\"><b>'+help.title+'</b><ol>';\n for(const s of help.steps) html += '<li>'+s+'</li>';\n html += '</ol></div>';\n }\n html += channelFields(ch, cfg);\n html += '</div></div>';\n wrap.innerHTML += html;\n }\n wrap.querySelectorAll('[data-ch-toggle]').forEach(inp=>{\n inp.addEventListener('change',e=>{\n const fields = wrap.querySelector('[data-ch-fields=\"'+e.target.dataset.chToggle+'\"]');\n if(fields) fields.style.display = e.target.checked ? '' : 'none';\n });\n });\n markEnvRefs(wrap);\n}\n"}
1
+ export function channelsJS(){return"\n/* ---- Channels ---- */\nconst CHANNEL_LIST = ['telegram','responses','mesh','whatsapp','discord','slack','signal','msteams','googlechat','line','matrix'];\nconst SUPPORTED_CHANNELS = ['telegram','responses','mesh','whatsapp'];\n\nconst CHANNEL_HELP = {\n telegram: {\n title: 'Telegram Setup',\n steps: [\n 'Open Telegram and message <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>',\n 'Send <code>/newbot</code>',\n 'Choose a display name, then a username (must end in <code>bot</code>)',\n 'BotFather replies with a <b>bot token</b> &mdash; paste it below',\n 'Optional: send <code>/setprivacy</code> &rarr; Disable, so the bot can see all group messages',\n '<b>DM Policy</b> controls who can talk to the bot: <b>open</b> = anyone can message, <b>allowlist</b> = only Telegram user IDs listed in Allow From can message',\n ]\n },\n discord: {\n title: 'Discord Setup',\n steps: [\n 'Go to <a href=\"https://discord.com/developers/applications\" target=\"_blank\">Discord Developer Portal</a> &rarr; <b>New Application</b>',\n '<b>Bot</b> tab &rarr; click <b>Reset Token</b> &rarr; copy the token and paste it below',\n 'On the same page, enable <b>Message Content Intent</b> under Privileged Gateway Intents',\n '<b>OAuth2 &rarr; URL Generator</b> &rarr; scope: <code>bot</code>, permission: <code>Send Messages</code>',\n 'Open the generated URL to invite the bot to your server',\n ]\n },\n slack: {\n title: 'Slack Setup',\n steps: [\n 'Go to <a href=\"https://api.slack.com/apps\" target=\"_blank\">api.slack.com/apps</a> &rarr; <b>Create New App</b> &rarr; From scratch',\n '<b>Socket Mode</b> &rarr; enable &rarr; generate an App-Level Token with scope <code>connections:write</code> &rarr; copy it (this is the <b>App Token</b>, starts with <code>xapp-</code>)',\n '<b>OAuth &amp; Permissions</b> &rarr; add Bot Token Scopes: <code>chat:write</code>, <code>im:history</code>, <code>im:read</code>',\n '<b>Event Subscriptions</b> &rarr; enable &rarr; subscribe to bot event <code>message.im</code>',\n 'Install the app to your workspace &rarr; copy the <b>Bot Token</b> (starts with <code>xoxb-</code>)',\n ]\n },\n whatsapp: {\n title: 'WhatsApp Setup',\n steps: [\n 'Set the <b>Auth Directory</b> below to a local folder (e.g. <code>./data/whatsapp</code>)',\n 'Enable the channel, save, then click <b>Connect WhatsApp</b> &mdash; a QR code will appear in a pop-up',\n 'Open WhatsApp on your phone &rarr; <b>Settings</b> &rarr; <b>Linked Devices</b> &rarr; scan the QR code',\n 'Once scanned, the pop-up confirms the connection. The session persists in the auth directory',\n '<b>DM Policy</b>: <b>open</b> = anyone can message, <b>allowlist</b> = only the phone numbers listed in Allow From can message (E.164 format, e.g. <code>+393331234567</code>)',\n ]\n },\n signal: {\n title: 'Signal Setup',\n steps: [\n 'Install <a href=\"https://github.com/bbernhard/signal-cli-rest-api\" target=\"_blank\">signal-cli-rest-api</a> and start it',\n 'Register or link a phone number via the signal-cli REST API',\n 'Enter the <b>API URL</b> (e.g. <code>http://localhost:8080</code>) and <b>Phone Number</b> below',\n ]\n },\n msteams: {\n title: 'Microsoft Teams Setup',\n steps: [\n 'Go to <a href=\"https://dev.teams.microsoft.com/\" target=\"_blank\">Teams Developer Portal</a> &rarr; <b>Apps</b> &rarr; New App',\n 'Under <b>App Features</b> &rarr; add a <b>Bot</b>',\n 'In Azure, register a new <b>Bot Channel Registration</b> &rarr; copy <b>App ID</b> and <b>App Secret</b>',\n 'Set the messaging endpoint to your server URL',\n 'Paste App ID and App Secret below',\n ]\n },\n googlechat: {\n title: 'Google Chat Setup',\n steps: [\n 'Go to <a href=\"https://console.cloud.google.com/\" target=\"_blank\">Google Cloud Console</a> &rarr; create or select a project',\n 'Enable the <b>Google Chat API</b>',\n '<b>Configuration</b> tab &rarr; set Bot URL to your server endpoint',\n 'Create a <b>Service Account</b> &rarr; download the JSON key file',\n 'Enter the path to the <b>credentials file</b> and <b>space ID</b> below',\n ]\n },\n line: {\n title: 'LINE Setup',\n steps: [\n 'Go to <a href=\"https://developers.line.biz/console/\" target=\"_blank\">LINE Developers Console</a> &rarr; create a Provider &rarr; create a <b>Messaging API</b> channel',\n 'Under <b>Messaging API</b> tab, issue a <b>Channel Access Token</b> &rarr; paste it below',\n 'Copy the <b>Channel Secret</b> from the <b>Basic Settings</b> tab',\n 'Set the <b>Webhook URL</b> to your server endpoint and enable <b>Use Webhook</b>',\n ]\n },\n matrix: {\n title: 'Matrix Setup',\n steps: [\n 'Create a bot account on your Matrix homeserver (e.g. via <code>register_new_matrix_user</code>)',\n 'Enter the <b>Homeserver URL</b> (e.g. <code>https://matrix.example.com</code>)',\n 'Enter the bot <b>User ID</b> (e.g. <code>@bot:example.com</code>) and <b>Access Token</b>',\n 'Invite the bot to the rooms you want it to participate in',\n ]\n },\n responses: {\n title: 'Responses API',\n steps: [\n 'This is a built-in HTTP API compatible with the OpenAI Responses format',\n 'Create an API token in the <b>Tokens</b> section to authenticate requests',\n 'Send requests to <code>POST http://&lt;host&gt;:&lt;port&gt;/v1/responses</code> with <code>Authorization: Bearer &lt;token&gt;</code>',\n ]\n },\n mesh: {\n title: 'Mesh (Inter-Agent Messaging)',\n steps: [\n 'Mesh enables direct agent-to-agent communication via a local WebSocket broker',\n 'Start the <b>hera-mesh</b> broker (<code>dev-start.sh</code> handles this automatically)',\n 'Set <b>Agent ID</b> to this agent\\'s name (e.g. <code>dante</code>, <code>beatrice</code>)',\n 'Set <b>Private Key</b> using an env var reference (e.g. <code>$\\{MESH_DANTE_KEY}</code>)',\n 'Add the env var in <b>Engine &rarr; Vars</b> with the Ed25519 private key (hex)',\n 'The agent must be registered in the broker\\'s <code>config/agents.yaml</code> with matching public key',\n ]\n },\n};\n\nfunction channelFields(ch, cfg) {\n const accts = cfg.accounts || {};\n const dfl = Object.values(accts)[0] || {};\n const aid = Object.keys(accts)[0] || 'default';\n let h = '';\n switch (ch) {\n case 'telegram':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n var dp = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dp==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dp==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(Telegram user IDs, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n break;\n case 'discord':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.token\" value=\"'+esc(dfl.token||'')+'\"></div>';\n break;\n case 'slack':\n h += '<div class=\"field\"><label>Bot Token <span style=\"color:var(--text-muted);font-weight:400\">(xoxb-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Token <span style=\"color:var(--text-muted);font-weight:400\">(xapp-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appToken\" value=\"'+esc(dfl.appToken||'')+'\"></div>';\n break;\n case 'whatsapp':\n h += '<div class=\"field\"><label>Auth Directory</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.authDir\" value=\"'+esc(dfl.authDir||'./data/whatsapp')+'\"></div>';\n var dpw = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dpw==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dpw==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(phone numbers in E.164 format, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n h += '<div class=\"field\"><button type=\"button\" class=\"btn btn-sm\" onclick=\"openWhatsAppQr()\" id=\"wa-connect-btn\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px\"><path d=\"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4\"/><polyline points=\"10 17 15 12 10 7\"/><line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\"/></svg>Connect WhatsApp</button></div>';\n break;\n case 'signal':\n h += '<div class=\"field\"><label>API URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.apiUrl\" value=\"'+esc(dfl.apiUrl||'http://localhost:8080')+'\"></div>';\n h += '<div class=\"field\"><label>Phone Number</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.phoneNumber\" value=\"'+esc(dfl.phoneNumber||'')+'\"></div>';\n break;\n case 'msteams':\n h += '<div class=\"field\"><label>App ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appId\" value=\"'+esc(dfl.appId||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Secret</label><input type=\"password\" data-ch-field=\"'+ch+'.'+aid+'.appSecret\" value=\"'+esc(dfl.appSecret||'')+'\"></div>';\n break;\n case 'googlechat':\n h += '<div class=\"field\"><label>Credentials File</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.credentialsFile\" value=\"'+esc(dfl.credentialsFile||'')+'\"></div>';\n h += '<div class=\"field\"><label>Space ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.spaceId\" value=\"'+esc(dfl.spaceId||'')+'\"></div>';\n break;\n case 'line':\n h += '<div class=\"field\"><label>Channel Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelAccessToken\" value=\"'+esc(dfl.channelAccessToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>Channel Secret</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelSecret\" value=\"'+esc(dfl.channelSecret||'')+'\"></div>';\n break;\n case 'matrix':\n h += '<div class=\"field\"><label>Homeserver URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.homeserverUrl\" value=\"'+esc(dfl.homeserverUrl||'')+'\"></div>';\n h += '<div class=\"field\"><label>User ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.userId\" value=\"'+esc(dfl.userId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.accessToken\" value=\"'+esc(dfl.accessToken||'')+'\"></div>';\n break;\n case 'responses':\n h += '<div class=\"field\"><label>Port</label><input type=\"number\" data-ch-field=\"'+ch+'.port\" value=\"'+(cfg.port||3000)+'\"></div>';\n break;\n case 'mesh':\n h += '<div class=\"field\"><label>Broker URL</label><input type=\"text\" data-ch-field=\"'+ch+'.brokerUrl\" value=\"'+esc(cfg.brokerUrl||'ws://127.0.0.1:3780/ws')+'\"></div>';\n h += '<div class=\"field\"><label>Agent ID</label><input type=\"text\" data-ch-field=\"'+ch+'.agentId\" value=\"'+esc(cfg.agentId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Private Key <span style=\"color:var(--text-muted);font-weight:400\">(use env var: <code>$\\{MESH_KEY}</code>)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.privateKey\" value=\"'+esc(cfg.privateKey||'')+'\"></div>';\n break;\n }\n return h;\n}\n\nasync function loadChannels(){\n currentConfig = await fetchAPI('/config');\n const wrap = document.getElementById('channelCards');\n wrap.innerHTML='';\n for(const ch of CHANNEL_LIST){\n const cfg = currentConfig.channels?.[ch]||{enabled:false};\n const help = CHANNEL_HELP[ch];\n const helpId = 'help-'+ch;\n const supported = SUPPORTED_CHANNELS.indexOf(ch) !== -1;\n if(!supported){ continue; }\n let html = '<div class=\"card\" data-channel=\"'+ch+'\">';\n html += '<div class=\"card-header\"><span class=\"card-title\" style=\"text-transform:capitalize\">'+esc(ch)+'</span>';\n html += '<label class=\"toggle\"><input type=\"checkbox\" data-ch-toggle=\"'+ch+'\" '+(cfg.enabled?'checked':'')+'><span></span></label></div>';\n html += '<div class=\"ch-fields\" data-ch-fields=\"'+ch+'\" style=\"'+(cfg.enabled?'':'display:none')+'\">';\n if(help){\n html += '<div style=\"margin-bottom:10px\"><button class=\"help-toggle\" type=\"button\" onclick=\"toggleHelp(this)\" data-help=\"'+helpId+'\" title=\"Setup guide\">?</button> <span style=\"font-size:13px;color:var(--text-muted)\">How to set up</span></div>';\n html += '<div class=\"help-panel\" id=\"'+helpId+'\"><b>'+help.title+'</b><ol>';\n for(const s of help.steps) html += '<li>'+s+'</li>';\n html += '</ol></div>';\n }\n html += channelFields(ch, cfg);\n html += '</div></div>';\n wrap.innerHTML += html;\n }\n wrap.querySelectorAll('[data-ch-toggle]').forEach(inp=>{\n inp.addEventListener('change',e=>{\n const fields = wrap.querySelector('[data-ch-fields=\"'+e.target.dataset.chToggle+'\"]');\n if(fields) fields.style.display = e.target.checked ? '' : 'none';\n });\n });\n markEnvRefs(wrap);\n}\n"}
package/dist/server.d.ts CHANGED
@@ -4,6 +4,7 @@ import { NodeSignatureDB } from "./auth/node-signature-db.js";
4
4
  import { SessionDB } from "./agent/session-db.js";
5
5
  import { ChannelManager } from "./gateway/channel-manager.js";
6
6
  import { WebChatChannel } from "./gateway/channels/webchat.js";
7
+ import { MeshChannel } from "./gateway/channels/mesh.js";
7
8
  import { AgentService } from "./agent/agent-service.js";
8
9
  import { NodeRegistry } from "./gateway/node-registry.js";
9
10
  import { MemoryManager } from "./memory/memory-manager.js";
@@ -35,6 +36,7 @@ export declare class Server {
35
36
  private whatsappConnected;
36
37
  private whatsappError;
37
38
  private webChatChannel;
39
+ private meshChannel;
38
40
  private autoRenewTimer;
39
41
  constructor(config: AppConfig);
40
42
  private getChatSetting;
@@ -66,6 +68,7 @@ export declare class Server {
66
68
  error?: string;
67
69
  };
68
70
  getWebChatChannel(): WebChatChannel | null;
71
+ getMeshChannel(): MeshChannel | null;
69
72
  triggerRestart(): Promise<void>;
70
73
  /**
71
74
  * Send a message to all known chats across all active channels.
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{readFileSync as e,writeFileSync as t,mkdirSync as s,existsSync as n}from"node:fs";import{join as o,resolve as i}from"node:path";import{TokenDB as r}from"./auth/token-db.js";import{NodeSignatureDB as a}from"./auth/node-signature-db.js";import{SessionDB as c}from"./agent/session-db.js";import{ChannelManager as h}from"./gateway/channel-manager.js";import{TelegramChannel as g}from"./gateway/channels/telegram/index.js";import{WhatsAppChannel as l}from"./gateway/channels/whatsapp.js";import{WebChatChannel as m}from"./gateway/channels/webchat.js";import{ResponsesChannel as d}from"./channels/responses.js";import{AgentService as f}from"./agent/agent-service.js";import{SessionManager as p}from"./agent/session-manager.js";import{buildPrompt as u,buildSystemPrompt as b}from"./agent/prompt-builder.js";import{ensureWorkspaceFiles as y,loadWorkspaceFiles as S}from"./agent/workspace-files.js";import{NodeRegistry as w}from"./gateway/node-registry.js";import{MemoryManager as v}from"./memory/memory-manager.js";import{MessageProcessor as M}from"./media/message-processor.js";import{loadSTTProvider as R}from"./stt/stt-loader.js";import{CommandRegistry as C}from"./commands/command-registry.js";import{NewCommand as A}from"./commands/new.js";import{CompactCommand as T}from"./commands/compact.js";import{ModelCommand as $,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as x}from"./commands/help.js";import{McpCommand as D}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as P}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as F}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{DebugA2UICommand as O}from"./commands/debuga2ui.js";import{DebugDynamicCommand as H}from"./commands/debugdynamic.js";import{CronService as B}from"./cron/cron-service.js";import{stripHeartbeatToken as L,isHeartbeatContentEffectivelyEmpty as Q}from"./cron/heartbeat-token.js";import{createServerToolsServer as W}from"./tools/server-tools.js";import{createCronToolsServer as z}from"./tools/cron-tools.js";import{createTTSToolsServer as G}from"./tools/tts-tools.js";import{createMemoryToolsServer as q}from"./tools/memory-tools.js";import{createBrowserToolsServer as V}from"./tools/browser-tools.js";import{createPicoToolsServer as J}from"./tools/pico-tools.js";import{createPlasmaClientToolsServer as X}from"./tools/plasma-client-tools.js";import{createConceptToolsServer as Y}from"./tools/concept-tools.js";import{BrowserService as Z}from"./browser/browser-service.js";import{MemorySearch as ee}from"./memory/memory-search.js";import{ConceptStore as te}from"./memory/concept-store.js";import{stripMediaLines as se}from"./utils/media-response.js";import{loadConfig as ne,loadRawConfig as oe,backupConfig as ie,resolveModelEntry as re,modelRefName as ae}from"./config.js";import{stringify as ce}from"yaml";import{createLogger as he}from"./utils/logger.js";import{SessionErrorHandler as ge}from"./agent/session-error-handler.js";import{initStickerCache as le}from"./gateway/channels/telegram/stickers.js";const me=he("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsFactory;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=null;conceptStore=null;whatsappQr=null;whatsappConnected=!1;whatsappError;webChatChannel=null;autoRenewTimer=null;constructor(e){this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.tokenDb=new r(e.dbPath),this.sessionDb=new c(e.dbPath),this.nodeSignatureDb=new a(e.dbPath),this.nodeRegistry=new w,this.sessionManager=new p(this.sessionDb),e.memory.enabled&&(this.memoryManager=new v(e.memoryDir));const t=R(e),n=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,n),le(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.createMemorySearch(e),this.browserService=new Z,this.conceptStore=new te(e.dataDir);const i=o(e.dataDir,"CONCEPTS.md");this.conceptStore.importFromTurtleIfEmpty(i);const g=o(e.agent.workspacePath,".plasma"),l=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,l?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:g,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),s(o(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(o(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(o(e.agent.workspacePath,".plugins"),{recursive:!0})}getChatSetting(e,t){return this.chatSettings.get(e)?.[t]}setChatSetting(e,t,s){const n=this.chatSettings.get(e)??{};n[t]=s,this.chatSettings.set(e,n)}createCronService(){return new B({storePath:this.config.cronStorePath,enabled:this.config.cron.enabled,defaultTimezone:this.config.timezone,onExecute:e=>this.executeCronJob(e),sessionReaper:{pruneStaleSessions:e=>this.sessionDb.pruneStaleCronSessions(e)}})}createMemorySearch(e){if(!e.memory.enabled||!e.memory.search.enabled)return;const t=e.memory.search,s=re(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",o=s?.baseURL||"";if(n)return this.memorySearch=new ee(e.memoryDir,e.dataDir,{apiKey:n,baseURL:o||void 0,embeddingModel:t.embeddingModel,embeddingDimensions:t.embeddingDimensions,prefixQuery:t.prefixQuery,prefixDocument:t.prefixDocument,updateDebounceMs:t.updateDebounceMs,embedIntervalMs:t.embedIntervalMs,maxResults:t.maxResults,maxSnippetChars:t.maxSnippetChars,maxInjectedChars:t.maxInjectedChars,rrfK:t.rrfK,l0:e.memory.l0??{enabled:!0,model:""}}),q(this.memorySearch);me.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 me.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 me.info(`Cron job "${e.name}": skipped by triage (${t.reason})`),{response:"",delivered:!1};me.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;me.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}=L(s,t);if(o)return me.info(`Cron job "${e.name}": response suppressed (HEARTBEAT_OK)`),{response:s,delivered:!1};n=i}if(t){const t=this.collectBroadcastTargets();me.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(r),this.sessionManager.resetSession(r),this.memoryManager&&this.memoryManager.clearSession(r),me.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=!Q(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 A),this.commandRegistry.register(new T);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,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{configDefaultModel:ae(this.config.agent.model),agentModel:n?.id??t,agentModelName:n?.name??ae(t),fallbackModel:o?.id??s,fallbackModelName:o?.name??(s?ae(s):void 0),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 j(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new D(()=>this.agentService.getToolServers())),this.commandRegistry.register(new x(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new H(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){me.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 d({host:this.config.host,port:this.config.channels.responses.port},this.tokenDb);this.channelManager.registerAdapter(e)}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]";me.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&me.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),r=u(i,void 0,{sessionKey:s,channel:e.channelName,chatId:e.chatId},this.config.timezone);me.debug(`[${s}] Prompt to agent (${r.text.length} chars): ${this.config.verboseDebugLogs?r.text:r.text.slice(0,15)+"..."}${r.images.length>0?` [+${r.images.length} image(s)]`:""}`);const a=o.model,c={sessionKey:s,channel:e.channelName,chatId:e.chatId,sessionId:o.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(s):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(s):""},h=S(this.config.dataDir),g={config:this.config,sessionContext:c,workspaceFiles:h,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(s,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},l=b(g),m=b({...g,mode:"minimal"});me.debug(`[${s}] System prompt (${l.length} chars): ${this.config.verboseDebugLogs?l:l.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,l,m,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 me.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),"";{const i=n.response||"Session corruption detected",r={sessionKey:s,sessionId:o.sessionId,error:new Error(i),timestamp:new Date},a=ge.analyzeError(r.error,r),c=ge.getRecoveryStrategy(a);return me.warn(`[${s}] ${c.message} (error: ${i})`),this.sessionManager.updateSessionId(s,""),c.clearSession&&(this.agentService.destroySession(s),this.memoryManager&&this.memoryManager.clearSession(s)),"clear_and_retry"!==c.action||t?"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one.":(me.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(me.debug(`[${s}] Response from agent (session=${n.sessionId}, len=${n.response.length}): ${this.config.verboseDebugLogs?n.response:n.response.slice(0,15)+"..."}`),n.sessionId&&this.sessionManager.updateSessionId(s,n.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(r.text||"[media]").trim();await this.memoryManager.append(s,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(s,"assistant",se(n.fullResponse??n.response))}if("max_turns"===n.errorType){const e="\n\n[MAX_TURNS] The agent reached the maximum number of turns. You can continue the conversation by sending another message.";return n.response?n.response+e:e.trim()}if("max_budget"===n.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return n.response?n.response+e:e.trim()}if("refusal"===n.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return n.response?n.response+e:e.trim()}if("max_tokens"===n.stopReason){const e="\n\n[TRUNCATED] The response was cut short because it exceeded the output token limit. You can ask me to continue.";return n.response?n.response+e:e.trim()}return n.response}catch(e){const t=e instanceof Error?e.message:String(e);return t.includes("SessionAgent closed")||t.includes("agent closed")?(me.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(me.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){me.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"),me.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{me.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),me.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}),me.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}),me.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?me.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?me.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):me.info("Heartbeat job disabled (config changed)"))}getConfig(){return this.config}getTokenDb(){return this.tokenDb}getSessionDb(){return this.sessionDb}getMemoryManager(){return this.memoryManager}getNodeRegistry(){return this.nodeRegistry}getNodeSignatureDb(){return this.nodeSignatureDb}getChannelManager(){return this.channelManager}getAgentService(){return this.agentService}getCronService(){return this.cronService}getCoderSkill(){return this.coderSkill}getWhatsAppQrState(){return{dataUrl:this.whatsappQr?.dataUrl??null,connected:this.whatsappConnected,error:this.whatsappError}}getWebChatChannel(){return this.webChatChannel}async triggerRestart(){me.info("Trigger restart requested");const e=ne();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();me.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),me.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){me.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&&(me.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>me.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){me.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){me.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}me.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){me.info("Reconfiguring server..."),this.cronService&&this.cronService.stop(),await this.channelManager.stopAll(),this.config=e,this.coderSkill=e.agent.builtinCoderSkill,this.sessionManager=new p(this.sessionDb),e.memory.enabled?this.memoryManager=new v(e.memoryDir):this.memoryManager=null;const t=R(e),s=this.memoryManager?this.memoryManager.saveFile.bind(this.memoryManager):null;this.messageProcessor=new M(t,s),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.agentService.destroyAll(),this.serverToolsFactory=()=>W(()=>this.triggerRestart(),this.config.timezone),this.cronService=this.createCronService(),this.stopMemorySearch(),this.createMemorySearch(e),await this.browserService.reconfigure(e.browser);const n=o(e.agent.workspacePath,".plasma"),i=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsFactory,this.cronService?()=>z(this.cronService,()=>this.config):void 0,this.sessionDb,e.tts.enabled?()=>G(()=>this.config):void 0,this.memorySearch?()=>q(this.memorySearch):void 0,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,e.browser?.enabled?()=>V({nodeRegistry:this.nodeRegistry,config:this.config}):void 0,i?()=>J({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=S(this.config.dataDir);return b({config:this.config,sessionContext:{sessionKey:"pico-subagent",channel:"internal",chatId:"subagent",sessionId:"",memoryFile:"",attachmentsDir:""},workspaceFiles:e,mode:"minimal",hasNodeTools:!1,hasMessageTools:!1})},getCallerMcpOptions:()=>{const e={};for(const t of this.agentService.getToolServers())"pico-tools"!==t.name&&(e[t.name]=t);return e}}):void 0,(e,t)=>X({plasmaRootDir:n,nodeRegistry:this.nodeRegistry,channel:e,chatId:t}),this.conceptStore?()=>Y(this.conceptStore):void 0),y(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),me.info("Server reconfigured successfully")}async stop(){me.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(),me.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 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 +1 @@
1
- import{createSdkMcpServer as e,tool as r}from"@anthropic-ai/claude-agent-sdk";import{z as t}from"zod";import{createLogger as n}from"../utils/logger.js";const o=n("MemoryTools");export function createMemoryToolsServer(n){return e({name:"memory-tools",version:"1.0.0",tools:[r("memory_search","Search conversation memory using hybrid keyword + semantic search. Returns matching chunks from past conversations with relevance scores. Results include chunkId — use memory_expand to get full chunk content when snippets are insufficient. Formulate clear, specific queries. For broad searches, call multiple times with different angles or phrasings.",{query:t.string().describe("The search query — be specific and descriptive for best results"),maxResults:t.number().optional().describe("Maximum number of results to return (default 6)")},async e=>{try{const r=await n.search(e.query,e.maxResults);if(0===r.length)return{content:[{type:"text",text:"No matching memories found."}]};const t=n.getMaxInjectedChars();let s=JSON.stringify(r,null,2);if(t>0&&s.length>t){const e=[...r];for(;e.length>1&&(e.pop(),s=JSON.stringify(e,null,2),!(s.length<=t)););s.length>t&&(s=s.slice(0,t)+"\n... [truncated — result exceeded maxInjectedChars limit]"),o.info(`memory_search: trimmed from ${r.length} to ${e.length} results to fit maxInjectedChars=${t}`)}return{content:[{type:"text",text:s}]}}catch(e){const r=e instanceof Error?e.message:String(e);return o.error(`memory_search error: ${r}`),{content:[{type:"text",text:`Search error: ${r}`}],isError:!0}}}),r("memory_expand","Get the full content of one or more memory chunks by their IDs (from memory_search results). Use this when a search snippet or L0 abstract is too short and you need the complete text.",{ids:t.array(t.number()).describe("Array of chunk IDs from memory_search results (chunkId field)")},async e=>{try{const r=n.expandChunks(e.ids);return 0===r.length?{content:[{type:"text",text:"No chunks found for the given IDs."}]}:{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){const r=e instanceof Error?e.message:String(e);return o.error(`memory_expand error: ${r}`),{content:[{type:"text",text:`Expand error: ${r}`}],isError:!0}}}),r("memory_get","Read a memory file (full or a specific slice) to get surrounding context after a search hit. Use the path from memory_search results.",{path:t.string().describe("Relative path to the memory file (from memory_search results)"),from:t.number().optional().describe("Starting line number (1-based). Omit to read from the beginning."),lines:t.number().optional().describe("Number of lines to read. Omit to read the entire file.")},async e=>{try{const r=n.readFile(e.path,e.from,e.lines);return{content:[{type:"text",text:JSON.stringify(r,null,2)}]}}catch(e){const r=e instanceof Error?e.message:String(e);return o.error(`memory_get error: ${r}`),{content:[{type:"text",text:`Read error: ${r}`}],isError:!0}}}),r("memory_record_access","Record that memory chunks were accessed/used in a response. This improves future retrieval by boosting frequently-used chunks. Automatically called by memory_expand, but can also be called manually for chunks from memory_search that were used without expanding.",{ids:t.array(t.number()).describe("Array of chunk IDs that were used")},async e=>{try{return n.recordAccess(e.ids),{content:[{type:"text",text:`Recorded access for ${e.ids.length} chunks.`}]}}catch(e){const r=e instanceof Error?e.message:String(e);return o.error(`memory_record_access error: ${r}`),{content:[{type:"text",text:`Error: ${r}`}],isError:!0}}})]})}
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as r}from"zod";import{createLogger as n}from"../utils/logger.js";const s=n("MemoryTools");export function createMemoryToolsServer(n){return e({name:"memory-tools",version:"1.0.0",tools:[t("memory_search","Search conversation memory using hybrid keyword + semantic search. Returns matching chunks from past conversations with relevance scores. Results include chunkId — use memory_expand to get full chunk content when snippets are insufficient. Formulate clear, specific queries. For broad searches, call multiple times with different angles or phrasings.",{query:r.string().describe("The search query — be specific and descriptive for best results"),maxResults:r.number().optional().describe("Maximum number of results to return (default 6)")},async e=>{try{const t=await n.search(e.query,e.maxResults);if(0===t.length)return{content:[{type:"text",text:"No matching memories found."}]};const r=n.getMaxInjectedChars();let o=JSON.stringify(t,null,2);if(r>0&&o.length>r){const e=[...t];for(;e.length>1&&(e.pop(),o=JSON.stringify(e,null,2),!(o.length<=r)););o.length>r&&(o=o.slice(0,r)+"\n... [truncated — result exceeded maxInjectedChars limit]"),s.info(`memory_search: trimmed from ${t.length} to ${e.length} results to fit maxInjectedChars=${r}`)}return{content:[{type:"text",text:o}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_search error: ${t}`),{content:[{type:"text",text:`Search error: ${t}`}],isError:!0}}}),t("memory_expand","Get the full content of one or more memory chunks by their IDs (from memory_search results). Use this when a search snippet or L0 abstract is too short and you need the complete text.",{ids:r.array(r.number()).describe("Array of chunk IDs from memory_search results (chunkId field)")},async e=>{try{const t=n.expandChunks(e.ids);return 0===t.length?{content:[{type:"text",text:"No chunks found for the given IDs."}]}:{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_expand error: ${t}`),{content:[{type:"text",text:`Expand error: ${t}`}],isError:!0}}}),t("memory_get","Read a memory file (full or a specific slice) to get surrounding context after a search hit. Use the path from memory_search results.",{path:r.string().describe("Relative path to the memory file (from memory_search results)"),from:r.number().optional().describe("Starting line number (1-based). Omit to read from the beginning."),lines:r.number().optional().describe("Number of lines to read. Omit to read the entire file.")},async e=>{try{const t=n.readFile(e.path,e.from,e.lines);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_get error: ${t}`),{content:[{type:"text",text:`Read error: ${t}`}],isError:!0}}}),t("memory_list","List memory files within a date range. Returns relative paths (compatible with memory_get) for all .md files whose filename starts with a date between `from` and `to` (inclusive). Searches across all channels and subfolders.",{from:r.string().describe("Start date (inclusive), format YYYY-MM-DD"),to:r.string().describe("End date (inclusive), format YYYY-MM-DD")},async e=>{try{const t=n.listFiles(e.from,e.to);return 0===t.length?{content:[{type:"text",text:"No memory files found in the given date range."}]}:{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_list error: ${t}`),{content:[{type:"text",text:`List error: ${t}`}],isError:!0}}}),t("memory_record_access","Record that memory chunks were accessed/used in a response. This improves future retrieval by boosting frequently-used chunks. Automatically called by memory_expand, but can also be called manually for chunks from memory_search that were used without expanding.",{ids:r.array(r.number()).describe("Array of chunk IDs that were used")},async e=>{try{return n.recordAccess(e.ids),{content:[{type:"text",text:`Recorded access for ${e.ids.length} chunks.`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return s.error(`memory_record_access error: ${t}`),{content:[{type:"text",text:`Error: ${t}`}],isError:!0}}})]})}
@@ -26,7 +26,7 @@ Single source of truth. Dreaming adds here, not elsewhere.
26
26
  If the message is the first in the session (no previous messages in context):
27
27
 
28
28
  1. `get_current_time` (always)
29
- 2. Read `memory/YYYY-MM-DD.md` today + yesterday (if they exist)
29
+ 2. Use `memory_list` with today and yesterday's dates, then `memory_get` to read the matching files (if any)
30
30
  3. If the message assumes shared context (people, projects, events, pronouns without referent):
31
31
  - `memory_search` with keywords from the message
32
32
  - `concept_query` on mentioned entities (in parallel)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hera-al/server",
3
- "version": "1.6.35",
3
+ "version": "1.6.37",
4
4
  "private": false,
5
5
  "description": "Hera Artificial Life — Multi-channel AI agent gateway with autonomous capabilities",
6
6
  "license": "MIT",