@hera-al/server 1.6.33 → 1.6.34
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.
|
@@ -54,5 +54,5 @@ export declare function resolvePlaceholders(template: string, vars: Record<strin
|
|
|
54
54
|
*
|
|
55
55
|
* Note: session_info is no longer included here — it's in the system prompt.
|
|
56
56
|
*/
|
|
57
|
-
export declare function buildPrompt(processed: ProcessedMessage, memoryContext?: string, sessionMeta?: SessionMeta): BuiltPrompt;
|
|
57
|
+
export declare function buildPrompt(processed: ProcessedMessage, memoryContext?: string, sessionMeta?: SessionMeta, timezone?: string): BuiltPrompt;
|
|
58
58
|
//# sourceMappingURL=prompt-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,filterForSubagent as l}from"./workspace-files.js";import{createLogger as c}from"../utils/logger.js";const d=c("PromptBuilder");export function buildSystemPrompt(c){const{config:u,sessionContext:p,mode:g,hasNodeTools:f,hasMessageTools:y,coderSkill:
|
|
1
|
+
import{hostname as e,type as n,release as t,arch as o}from"node:os";import{modelRefName as s}from"../config.js";import{loadTemplate as a,loadBuiltInTools as r,formatWorkspaceFiles as i,filterForSubagent as l}from"./workspace-files.js";import{createLogger as c}from"../utils/logger.js";const d=c("PromptBuilder");export function buildSystemPrompt(c){const{config:u,sessionContext:p,mode:g,hasNodeTools:f,hasMessageTools:y,coderSkill:S,subagentTask:T,toolServers:_}=c,E="minimal"===g?"SYSTEM_PROMPT_SUBAGENT.md":"SYSTEM_PROMPT.md",w=a(u.dataDir,E),I="minimal"===g?l(c.workspaceFiles):c.workspaceFiles;var b;const O=resolvePlaceholders(w,{SESSION_KEY:p.sessionKey,CHANNEL:p.channel,CHAT_ID:p.chatId,SESSION_ID:p.sessionId||"(new session)",MODEL:s(u.agent.model),HOSTNAME:e(),OS:`${n()} ${t()} (${o()})`,WORKSPACE_DIR:u.agent.workspacePath,DATA_DIR:u.dataDir,MEMORY_FILE:p.memoryFile||"(memory disabled)",ATTACHMENTS_DIR:p.attachmentsDir||"(memory disabled)",TIMEZONE:u.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,CURRENT_DATE:(new Date).toLocaleDateString("en-US",{timeZone:u.timezone||void 0,weekday:"long",year:"numeric",month:"long",day:"numeric"}),SUBAGENT_TASK:T??"",NODE_TOOLS_INSTRUCTIONS:f?"# Remote Nodes\n\nYou have access to remote nodes — external machines that you can control. Use the node tools to discover and interact with them.\n\n## Available tools\n\n- **list_nodes**: Call this to see which nodes are currently connected. Returns each node's ID, name, platform, hostname, and available commands. Always call this first before trying to execute commands, so you know which nodes are online and what their IDs are.\n\n- **node_exec**: Execute a command on a specific node. You must provide the nodeId (from list_nodes), the command name, and its parameters.\n\n## Supported commands\n\n- **shell.run**: Run a shell command on the node. Params: { cmd: string, args?: string[], cwd?: string, timeout?: number, env?: Record<string,string> }. Returns { stdout, stderr, exitCode }.\n- **shell.which**: Check if a binary exists on the node. Params: { cmd: string }. Returns { path } or null.\n\n## Guidelines\n\n- Always call list_nodes first to discover available nodes and their IDs. Do not guess node IDs.\n- When a user asks to run something on a remote machine, a node, or a specific hostname, call list_nodes to see what's online, then use node_exec with the appropriate nodeId.\n- If multiple nodes are connected, ask the user which one to use when the intent is ambiguous.\n- If no nodes are connected, inform the user that no remote nodes are available.\n- Report command results clearly: show stdout, note any stderr, and mention non-zero exit codes.":"",MESSAGE_TOOLS_INSTRUCTIONS:y?"# Messaging\n\nYou have tools to send messages to chat channels. Each message you process includes a <session_info> block with the current channel and chatId.\n\n## Available tools\n\n- **send_message**: Send a text message to a specific channel and chat. Use the channel and chatId from the session context to reply on the current conversation. You can also send to a different channel or chatId if instructed.\n\n- **list_channels**: List all registered channels. Returns each channel's name and whether it is active.\n\n## Guidelines\n\n- Use send_message when you need to proactively send a message outside of the normal response flow (e.g. notifications, forwarding, or sending to a different chat).\n- Your normal response text is already delivered to the user. Only use send_message for additional messages or cross-channel communication.\n- The channel and chatId from the session context identify the current conversation. Use them to send follow-up messages to the same chat.\n- If the user asks you to message someone on a different channel or chat, use the appropriate channel name and chatId.\n- Never spam or send unsolicited messages. Only send when explicitly asked or when it is clearly part of the task.":"",HEARTBEAT_INSTRUCTIONS:u.cron.enabled?(b=u.cron.heartbeat.message,`# Heartbeats\n\nHeartbeat prompt: ${b}\nIf you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:\nHEARTBEAT_OK\nThe system treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack and may suppress it (not deliver to the user).\nIf something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.`):"",HEARTBEAT_PROMPT:u.cron.enabled?u.cron.heartbeat.message:"",CLAUDE_BUILT_IN_TOOLS:S?"":r(u.dataDir,g),SEARCH_IN_MEMORIES:"builtin-only"!==u.memory.recallStrategy?"## Memory Search Tools\n\nYou have access to memory search tools for recalling past conversations and knowledge:\n\n- `memory_search` — semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, and score. No full file payload is returned.\n- `memory_get` — reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected.\n\nUse `memory_search` to find relevant past context before answering questions that might relate to previous conversations. Use `memory_get` to read the full content of a memory file when a search snippet is not enough.":"",AVAILABLE_TOOLS:m(_),RUNTIME_LINE:h(u,p),WORKSPACE_FILES:i(I)}).replace(/\n{3,}/g,"\n\n");return d.debug(`System prompt built (mode=${g}, template=${E}, length=${O.length})`),O}export function resolvePlaceholders(e,n){return e.replace(/\{\{(\w+)\}\}/g,(e,t)=>t in n?n[t]:(d.warn(`Unknown placeholder: {{${t}}}`),`{{${t}}}`))}function m(e){if(!e||0===e.length)return"";const n=[];for(const t of e){const e=t,o=e.instance?._registeredTools;if(o)for(const[e,t]of Object.entries(o)){const o=t.description||"",s=t.inputSchema?.def?.shape,a=[];if(s)for(const[e,n]of Object.entries(s)){let t=n.type||"unknown";"ZodNumber"===t?t="number":"ZodString"===t?t="string":"ZodBoolean"===t?t="boolean":"ZodArray"===t?t="array":"ZodObject"===t?t="object":"ZodEnum"===t&&(t="enum");const o=n.isOptional?.()??!1;a.push(`${e}${o?"?":""}: ${t}`)}const r=a.length>0?`Params: { ${a.join(", ")} }`:"No parameters.",i=o.match(/\.\s*(Returns?\s.+?)\.?\s*$/i),l=i?i[1].trim():"";let c=`- **${e}**: ${i?o.slice(0,i.index).trim():o.trim()}. ${r}`;l&&(c+=`. ${l}.`),n.push(c)}}return 0===n.length?"":n.join("\n")}function h(a,r){return`host=${e()} | os=${n()} ${t()} (${o()}) | model=${s(a.agent.model)} | channel=${r.channel} | session=${r.sessionKey}`}export function buildPrompt(e,n,t,o){const s=[],a=[];if(o){const n=e.contentBlocks.find(e=>"text"===e.type)?.text??"",t=/^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(n),a=/Current time: /.test(n);if(!t&&!a){const e=new Date,n=new Intl.DateTimeFormat("en-US",{timeZone:o,weekday:"short"}).format(e),t=e.toLocaleString("en-US",{timeZone:o,year:"numeric"}),a=e.toLocaleString("en-US",{timeZone:o,month:"2-digit"}),r=e.toLocaleString("en-US",{timeZone:o,day:"2-digit"}),i=e.toLocaleString("en-GB",{timeZone:o,hour:"2-digit",minute:"2-digit",hour12:!1});s.push(`[${n} ${t}-${a}-${r} ${i}]`)}}n&&s.push("<conversation_history>",n,"</conversation_history>","");for(const n of e.contentBlocks)"text"===n.type&&n.text?s.push(n.text):"image"===n.type&&n.imageBase64&&a.push({base64:n.imageBase64,mimeType:n.imageMimeType??"image/jpeg"});const r=e.savedFiles.filter(e=>!e.endsWith(".tgs"));if(r.length>0){s.push(""),s.push("Files available in the current working directory:");for(const e of r)s.push(`- ${e}`)}return{text:s.join("\n"),images:a}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Hono as e}from"hono";import{serve as t}from"@hono/node-server";import{getCookie as r,setCookie as n,deleteCookie as o}from"hono/cookie";import{stringify as s}from"yaml";import{writeFileSync as i,readFileSync as a,readdirSync as c,statSync as l,existsSync as d,rmSync as u,mkdirSync as f,unlinkSync as p,cpSync as m}from"node:fs";import{execFileSync as h}from"node:child_process";import{createHash as g}from"node:crypto";import{resolve as y,join as j,basename as v}from"node:path";import{WebSocketServer as k}from"ws";import{isHeartbeatContentEffectivelyEmpty as w}from"../cron/heartbeat-token.js";import{buildWebChatId as $}from"../gateway/channels/webchat.js";import{loadConfig as S,loadRawConfig as C,backupConfig as b,resolveModelEntry as N}from"../config.js";import{renderSPA as I}from"./ui.js";import{buildSystemPrompt as T}from"../agent/prompt-builder.js";import{loadWorkspaceFiles as q}from"../agent/workspace-files.js";import{resolveBundledDir as D}from"../utils/package-paths.js";import{readKey as P,isDefaultKey as A,validateKey as x,regenerateKey as O,createSession as E,validateSession as R,validateCsrf as K,getCsrfToken as _,destroySession as F,checkRateLimit as L,recordLoginFailure as B,resetLoginAttempts as M}from"./auth.js";import{createLogger as z,getLogsDir as U,getLogLevel as W,setLogLevel as J}from"../utils/logger.js";const H=z("Nostromo"),Y="nostromo_session";export class Nostromo{app;httpServer=null;wss=null;host;port;server;nodeRegistry;startedAt;pendingNodes=new Map;configHash="";configPath;basePath;constructor(t,r,n,o,s="/nostromo"){this.server=t,this.host=r,this.port=n,this.nodeRegistry=o,this.basePath=s.replace(/\/+$/,"")||"/",this.startedAt=Date.now(),this.app=new e,this.configPath=y(process.cwd(),"config.yaml"),this.configHash=this.hashConfigFile(),P(),this.setupRoutes()}hashConfigFile(){try{const e=a(this.configPath,"utf-8");return g("sha256").update(e).digest("hex")}catch{return""}}setupRoutes(){const e="/"===this.basePath?"":this.basePath,t=e?this.app.basePath(e):this.app;e&&this.app.get("/",t=>t.redirect(e+"/")),t.use("*",async(t,r)=>{const n=Date.now();await r();const o=Date.now()-n;t.req.path===e+"/api/logs/current"||t.req.path===e+"/api/config/check"||t.req.path===e+"/api/config"||t.req.path===e+"/api/memory-files"||t.req.path===e+"/api/status"||t.req.path===e+"/api/auth/session"?H.debug(`${t.req.method} ${t.req.path} ${t.res.status} ${o}ms`):H.info(`${t.req.method} ${t.req.path} ${t.res.status} ${o}ms`)}),t.use("/api/*",async(e,t)=>{const r=e.req.header("Origin");return r&&(e.header("Access-Control-Allow-Origin",r),e.header("Access-Control-Allow-Credentials","true"),e.header("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS"),e.header("Access-Control-Allow-Headers","Content-Type, Authorization, X-CSRF-Token")),"OPTIONS"===e.req.method?new Response(null,{status:204}):t()}),t.get("/",t=>t.html(I(A(),e))),t.post("/api/auth/login",async t=>{const r=t.req.header("x-forwarded-for")||t.req.header("x-real-ip")||"unknown",o=L(r);if(!o.allowed)return t.json({error:`Too many attempts. Try again in ${o.retryAfter}s`},429);const s=(await t.req.json()).key;if(!x(s))return B(r),t.json({error:"Invalid access key"},401);M(r);const{sessionToken:i,csrfToken:a}=E();if(n(t,Y,i,{httpOnly:!0,sameSite:"Lax",path:e||"/",maxAge:86400}),A()){const e=O();return H.info("Default key used; rotated to new key"),t.json({ok:!0,newKey:e,csrfToken:a})}return t.json({ok:!0,csrfToken:a})}),t.post("/api/auth/logout",async t=>{const n=r(t,Y);return n&&F(n),o(t,Y,{path:e||"/"}),t.json({ok:!0})}),t.get("/api/auth/session",e=>{const t=r(e,Y);if(!R(t))return e.json({valid:!1},401);const n=_(t);return e.json({valid:!0,csrfToken:n})}),t.use("/api/*",async(t,n)=>{if(t.req.path.startsWith(e+"/api/auth/"))return n();const o=t.req.header("Authorization");if(o?.startsWith("Bearer ")){const e=o.slice(7);return this.server.getTokenDb().validateToken(e,"nostromo")?n():t.json({error:"Invalid API token"},401)}const s=r(t,Y);if(!R(s))return t.json({error:"Unauthorized"},401);if("POST"===t.req.method||"PUT"===t.req.method||"DELETE"===t.req.method){const e=t.req.header("X-CSRF-Token");if(!K(s,e))return t.json({error:"Invalid CSRF token"},403)}return n()}),t.get("/api/config",e=>{const t=C(),r=this.server.getConfig();return t.dataDir=r.dataDir,t.gmabPath=r.gmabPath,e.json(t)}),t.put("/api/config",async e=>{try{const t=await e.req.json();if(t.cron?.heartbeat?.enabled&&(!t.cron.heartbeat.message||t.cron.heartbeat.message.trim().length<15))return e.json({error:"Heartbeat message is required and must be at least 15 characters"},400);const{dataDir:r,dbPath:n,memoryDir:o,cronStorePath:c,...l}=t,u=function(e){const t={};if(e.channels)for(const[r,n]of Object.entries(Z)){const o=e.channels[r];if(o?.accounts)for(const[e,s]of Object.entries(o.accounts))for(const o of n){const n=s[o.field];if("string"==typeof n&&n.length>0&&!V(n)){const i=`${r.toUpperCase()}_${e.toUpperCase().replace(/-/g,"_")}_${o.envSuffix}`;t[i]=n,s[o.field]=`\${${i}}`}}}if(Array.isArray(e.models))for(const r of e.models){if((r.types||["external"]).includes("internal"))continue;const e=r.apiKey;if("string"==typeof e&&e.length>0&&!V(e)){const n=r.useEnvVar||"OPENAI_API_KEY";t[n]=e,r.apiKey=`\${${n}}`,r.useEnvVar||(r.useEnvVar=n)}const n=r.fastProxyApiKey;if("string"==typeof n&&n.length>0&&!V(n)){const e="FAST_PROXY_APIKEY";t[e]=n,r.fastProxyApiKey=`\${${e}}`}}if(e.stt?.["openai-whisper"]?.apiKey){const r=e.stt["openai-whisper"].apiKey;if("string"==typeof r&&r.length>0&&!V(r)){const n="STT_OPENAI_WHISPER_API_KEY";t[n]=r,e.stt["openai-whisper"].apiKey=`\${${n}}`}}if(Object.keys(t).length>0){!function(e){const t=y(process.cwd(),".env");let r=d(t)?a(t,"utf-8"):"";for(const[t,n]of Object.entries(e)){const e=new RegExp(`^${t}=.*$`,"m");e.test(r)?r=r.replace(e,`${t}=${n}`):(r.length>0&&!r.endsWith("\n")&&(r+="\n"),r+=`${t}=${n}\n`)}i(t,r,"utf-8")}(t);for(const[e,r]of Object.entries(t))process.env[e]=r;H.info(`Protected ${Object.keys(t).length} secret(s) → .env`)}return e}(l),f=s(u),p=y(process.cwd(),"config.yaml");return b(p),i(p,f,"utf-8"),H.info("Config updated via Nostromo"),e.json({ok:!0})}catch(t){return H.error(`Config save failed: ${t}`),e.json({error:"Failed to save configuration"},500)}}),t.get("/api/tokens",e=>{const t=this.server.getTokenDb().listTokens();return e.json(t)}),t.post("/api/tokens",async e=>{const t=await e.req.json(),r=this.server.getTokenDb().createToken({userId:t.userId,channel:t.channel,label:t.label});return e.json(r,201)}),t.delete("/api/tokens/:id",e=>{const t=parseInt(e.req.param("id"));return this.server.getTokenDb().deleteToken(t),e.json({ok:!0})}),t.post("/api/tokens/:id/revoke",e=>{const t=parseInt(e.req.param("id"));return this.server.getTokenDb().revokeToken(t),e.json({ok:!0})}),t.post("/api/tokens/:id/approve",e=>{const t=parseInt(e.req.param("id"));return this.server.getTokenDb().approveToken(t),e.json({ok:!0})}),t.get("/api/key",e=>e.json({key:P()})),t.post("/api/key/regenerate",e=>{const t=O();return e.json({key:t})}),t.get("/api/whatsapp/qr",e=>{const t=this.server.getWhatsAppQrState();return e.json(t)}),t.get("/api/status",e=>{const t=Date.now()-this.startedAt,r=Math.floor(t/1e3),n=`${Math.floor(r/3600)}h ${Math.floor(r%3600/60)}m ${r%60}s`;return e.json({status:"online",uptime:n,uptimeMs:t})}),t.post("/api/server/restart",async e=>{try{H.info("Server restart requested via Nostromo");const t=S();return await this.server.reconfigure(t),this.startedAt=Date.now(),this.configHash=this.hashConfigFile(),H.info("Server restarted successfully via Nostromo"),e.json({ok:!0})}catch(t){return H.error(`Server restart failed: ${t}`),e.json({error:`Restart failed: ${t instanceof Error?t.message:String(t)}`},500)}}),t.get("/api/config/check",e=>{const t=this.hashConfigFile(),r=t!==this.configHash&&""!==t;return e.json({restartNeeded:r})}),t.get("/api/sessions",e=>{const t=this.server.getSessionDb().listSessions().map(e=>{const[t,...r]=e.sessionKey.split(":"),n=r.join(":"),o=new Date(e.createdAt+"Z"),s=e=>String(e).padStart(2,"0"),i=`${o.getFullYear()}${s(o.getMonth()+1)}${s(o.getDate())}_${s(o.getHours())}${s(o.getMinutes())}`,a=`${e.sdkSessionId??"pending"}:${n}:${t}:${i}`;return{sessionKey:e.sessionKey,channelName:t,chatId:n,sdkSessionId:e.sdkSessionId,modelOverride:e.modelOverride,createdAt:e.createdAt,lastActivity:e.lastActivity,displayId:a}});return e.json(t)}),t.post("/api/sessions/:sessionKey/messages",async e=>{try{const t=e.req.param("sessionKey"),r=(await e.req.json()).message;if(!r)return e.json({error:"message is required"},400);const n=this.server.getSessionDb();if(!n.getBySessionKey(t))return e.json({error:"Session not found"},404);const[o,...s]=t.split(":"),i=s.join(":"),a={chatId:i,userId:"nostromo",channelName:o,text:r,attachments:[]},c=await this.server.handleMessage(a),l=this.server.getChannelManager();return await l.sendToChannel(o,i,c),e.json({ok:!0,sessionKey:t,response:c})}catch(t){return H.error(`Cross-session message failed: ${t}`),e.json({error:"Failed to process message"},500)}}),t.get("/api/nodes",e=>{const t=this.nodeRegistry.listNodes();return e.json(t)}),t.get("/api/nodes/signatures",e=>{const t=this.server.getNodeSignatureDb(),r=e.req.query("status"),n=r?t.listByStatus(r):t.listAll();return e.json(n)}),t.post("/api/nodes/signatures/:id/approve",e=>{const t=parseInt(e.req.param("id")),r=this.server.getNodeSignatureDb();return r.getById(t)?(r.approve(t),this.notifyPairingChange(t,"approved"),e.json({ok:!0})):e.json({error:"Not found"},404)}),t.post("/api/nodes/signatures/:id/revoke",e=>{const t=parseInt(e.req.param("id")),r=this.server.getNodeSignatureDb();return r.getById(t)?(r.revoke(t),this.notifyPairingChange(t,"revoked"),e.json({ok:!0})):e.json({error:"Not found"},404)}),t.delete("/api/nodes/signatures/:id",e=>{const t=parseInt(e.req.param("id")),r=this.server.getNodeSignatureDb();return r.getById(t)?(this.notifyPairingChange(t,"revoked"),r.delete(t),e.json({ok:!0})):e.json({error:"Not found"},404)}),t.get("/api/cron/status",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);const r=await t.status();return e.json(r)}),t.get("/api/cron/jobs",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);const r="true"===e.req.query("includeDisabled"),n=await t.list({includeDisabled:r});return e.json(n)}),t.post("/api/cron/jobs",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=await e.req.json(),n=await t.add(r);return e.json(n,201)}catch(t){return H.error(`Cron job add failed: ${t}`),e.json({error:String(t)},400)}}),t.put("/api/cron/jobs/:id",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=e.req.param("id"),n=await e.req.json(),o=await t.update(r,n);return e.json(o)}catch(t){return H.error(`Cron job update failed: ${t}`),e.json({error:String(t)},400)}}),t.delete("/api/cron/jobs/:id",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=e.req.param("id"),n=await t.remove(r);return e.json(n)}catch(t){return H.error(`Cron job remove failed: ${t}`),e.json({error:String(t)},400)}}),t.post("/api/cron/jobs/:id/run",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=e.req.param("id"),n="due"===(await e.req.json().catch(()=>({}))).mode?"due":"force",o=await t.run(r,n);return e.json(o)}catch(t){return H.error(`Cron job run failed: ${t}`),e.json({error:String(t)},400)}}),t.post("/api/heartbeat/simulate",async e=>{const t=this.server.getConfig(),r=j(t.dataDir,"HEARTBEAT.md");let n="",o=!1;try{d(r)&&(o=!0,n=a(r,"utf-8"))}catch{}const s=!(o&&w(n));return e.json({accepted:s,fileExists:o,content:n})}),t.post("/api/inject",async e=>{try{const t=await e.req.json(),{channel:r,chatId:n,text:o}=t;if(!r||!n||!o)return e.json({error:"channel, chatId, and text are required"},400);const s={chatId:n,userId:"inject",channelName:r,text:o,attachments:[]},i=await this.server.handleMessage(s),a=this.server.getChannelManager();await a.sendToChannel(r,n,i);const c=`${r}:${n}`;return e.json({ok:!0,sessionKey:c,response:i})}catch(t){return H.error(`Inject message failed: ${t}`),e.json({error:"Failed to inject message"},500)}}),t.get("/api/internal-tools",e=>{const t=this.server.getAgentService().getToolServers(),r=[];for(const e of t){const t=e,n=t.name||"unknown",o=t.instance?._registeredTools;if(!o)continue;const s=[];for(const[e,t]of Object.entries(o)){const r=[],n=t.inputSchema?.def?.shape;if(n)for(const[e,t]of Object.entries(n)){let n=t.type||"unknown";const o=t.isOptional?.()??!1;"optional"===n&&t.def?.innerType&&(n=t.def.innerType.type||"unknown","enum"===n&&t.def.innerType.def?.entries&&(n=Object.keys(t.def.innerType.def.entries).join("|"))),"enum"===n&&t.def?.entries&&(n=Object.keys(t.def.entries).join("|")),r.push({name:e,type:n,required:!o,description:t.description||""})}s.push({name:e,description:t.description||"",params:r})}r.push({server:n,tools:s})}return e.json(r)});const g=["AGENTS.md","SOUL.md","TOOLS.md","IDENTITY.md","USER.md","HEARTBEAT.md","BOOTSTRAP.md","MEMORY.md"],k=["SYSTEM_PROMPT.md","SYSTEM_PROMPT_SUBAGENT.md","CBINT.json"];t.get("/api/workspace-files",e=>{const t=this.server.getConfig().dataDir,r=[];for(const e of g){const n=j(t,e);if(d(n))try{r.push({name:e,content:a(n,"utf-8"),exists:!0,isTemplate:!1})}catch{r.push({name:e,content:"",exists:!1,isTemplate:!1})}else r.push({name:e,content:"",exists:!1,isTemplate:!1})}for(const e of k){const n=j(t,".templates",e);if(d(n))try{r.push({name:e,content:a(n,"utf-8"),exists:!0,isTemplate:!0})}catch{r.push({name:e,content:"",exists:!1,isTemplate:!0})}else r.push({name:e,content:"",exists:!1,isTemplate:!0})}return e.json(r)}),t.put("/api/workspace-files/:name",async e=>{const t=e.req.param("name"),r=await e.req.json(),n=r.content,o=!0===r.isTemplate;if(null==n)return e.json({error:"content is required"},400);const s=this.server.getConfig().dataDir;let a;if(o){if(!k.includes(t))return e.json({error:"Invalid template name"},400);a=j(s,".templates",t)}else{if(!g.includes(t))return e.json({error:"Invalid file name"},400);a=j(s,t)}try{return i(a,n,"utf-8"),H.info(`Workspace file saved via Nostromo: ${a}`),e.json({ok:!0})}catch(t){return H.error(`Failed to save workspace file: ${t}`),e.json({error:"Failed to save file"},500)}}),t.post("/api/plugins/upload",async e=>{try{const t=this.server.getConfig(),r=await e.req.formData(),n=(r.get("folderName")||"").trim();if(!n)return e.json({error:"folderName is required"},400);if(n.includes("..")||n.includes("/")||n.includes("\\"))return e.json({error:"Invalid folder name"},400);const o=r.getAll("files"),s=r.getAll("paths");if(0===o.length)return e.json({error:"No files provided"},400);if(o.length!==s.length)return e.json({error:"files and paths count mismatch"},400);const c=(r.get("basePath")||"").trim()||j(t.agent.workspacePath,".plugins"),l=j(c,n);f(l,{recursive:!0});for(let e=0;e<o.length;e++){const t=o[e],r=s[e];if(r.includes(".."))continue;const n=j(l,r),a=j(n,"..");f(a,{recursive:!0});const c=Buffer.from(await t.arrayBuffer());i(n,c)}let u=n,p=n;const m=j(l,".claude-plugin","plugin.json");if(d(m))try{const e=a(m,"utf-8"),t=JSON.parse(e);t.name&&(u=t.name),t.description&&(p=t.description)}catch{}return e.json({ok:!0,path:l,name:u,description:p})}catch(t){return H.error(`Plugin upload failed: ${t}`),e.json({error:"Upload failed"},500)}}),t.post("/api/plugins/delete-folder",async e=>{try{const t=((await e.req.json()).path||"").trim();if(!t)return e.json({error:"path is required"},400);const r=y(t);return d(r)?l(r).isDirectory()?(u(r,{recursive:!0,force:!0}),H.info(`Plugin folder deleted via Nostromo: ${r}`),e.json({ok:!0})):e.json({error:"Path is not a directory"},400):e.json({ok:!0,message:"Directory already gone"})}catch(t){return H.error(`Plugin folder delete failed: ${t}`),e.json({error:"Failed to delete folder"},500)}}),t.get("/api/plugins/download/:index",e=>{try{const t=parseInt(e.req.param("index"),10),r=this.server.getConfig(),n=r.agent?.plugins||[];if(isNaN(t)||t<0||t>=n.length)return e.json({error:"Invalid plugin index"},400);const o=n[t].path;if(!o)return e.json({error:"Plugin has no path"},400);const s=y(o);if(!d(s)||!l(s).isDirectory())return e.json({error:"Plugin folder not found"},404);const i=v(s),a=j(s,".."),c=h("zip",["-r","-q","-",i],{cwd:a,maxBuffer:104857600});return new Response(c,{headers:{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${i}.zip"`}})}catch(t){return H.error(`Plugin download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.post("/api/plugins/verify-path",async e=>{try{const t=((await e.req.json()).path||"").trim();if(!t)return e.json({ok:!1,error:"Path is empty"});const r=y(t);return d(r)?l(r).isDirectory()?e.json({ok:!0,path:r}):e.json({ok:!1,error:"Path is not a directory"}):e.json({ok:!1,error:"Directory does not exist"})}catch(t){return e.json({ok:!1,error:"Invalid path"})}}),t.get("/api/plugins/info",e=>{const t=this.server.getConfig().agent.plugins||[],r=[];for(let e=0;e<t.length;e++){const n=t[e];let o=n.name,s=n.description||"",i=!1;try{if(d(n.path)&&l(n.path).isDirectory()){i=c(n.path).length>0;const e=j(n.path,".claude-plugin","plugin.json");if(d(e))try{const t=a(e,"utf-8"),r=JSON.parse(t);r.name&&(o=r.name),r.description&&!s&&(s=r.description)}catch{}}}catch{}r.push({index:e,name:o,description:s,valid:i})}return e.json(r)}),t.get("/api/commands",e=>{const t=this.server.getConfig(),r=j(t.agent.workspacePath,".claude","commands"),n=[];if(!d(r))return e.json(n);const o=(e,t)=>{try{const r=c(e);for(const s of r){const r=j(e,s);if(l(r).isDirectory())o(r,t?`${t}/${s}`:s);else if(s.endsWith(".md")){const e=t?`${t}/${s}`:s;let o=s.replace(/\.md$/,""),i="",c="";try{const e=G(a(r,"utf-8"));e.name&&(o=e.name),e.description&&(i=e.description),e.model&&(c=e.model)}catch{}n.push({file:e,folder:t,name:o,description:i,model:c})}}}catch{}};return o(r,""),n.sort((e,t)=>e.folder!==t.folder?e.folder.localeCompare(t.folder):e.name.localeCompare(t.name)),e.json(n)}),t.post("/api/commands/delete",async e=>{try{const t=((await e.req.json()).path||"").trim();if(!t||t.includes(".."))return e.json({error:"Invalid path"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","commands",t);if(!d(n))return e.json({error:"Command not found"},404);if(l(n).isDirectory())u(n,{recursive:!0,force:!0});else{p(n);const e=j(n,"..");if(e!==j(r.agent.workspacePath,".claude","commands"))try{0===c(e).length&&u(e,{recursive:!0,force:!0})}catch{}}return H.info(`Command deleted via Nostromo: ${t}`),e.json({ok:!0})}catch(t){return H.error(`Failed to delete command: ${t}`),e.json({error:"Failed to delete command"},500)}}),t.post("/api/commands/upload",async e=>{try{const t=this.server.getConfig(),r=await e.req.formData(),n=(r.get("folderName")||"").trim();if(!n)return e.json({error:"folderName is required"},400);if(n.includes("..")||n.includes("/")||n.includes("\\"))return e.json({error:"Invalid folder name"},400);const o=r.getAll("files"),s=r.getAll("paths");if(0===o.length)return e.json({error:"No files provided"},400);if(o.length!==s.length)return e.json({error:"files and paths count mismatch"},400);const a=j(t.agent.workspacePath,".claude","commands",n);f(a,{recursive:!0});for(let e=0;e<o.length;e++){const t=o[e],r=s[e];if(r.includes(".."))continue;const n=j(a,r),c=j(n,"..");f(c,{recursive:!0});const l=Buffer.from(await t.arrayBuffer());i(n,l)}return H.info(`Command folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return H.error(`Command upload failed: ${t}`),e.json({error:"Upload failed"},500)}}),t.get("/api/commands/download/:folder",e=>{try{const t=e.req.param("folder");if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","commands"),o=j(n,t);if(!d(o)||!l(o).isDirectory())return e.json({error:"Command folder not found"},404);const s=h("zip",["-r","-q","-",t],{cwd:n,maxBuffer:104857600});return new Response(s,{headers:{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${t}.zip"`}})}catch(t){return H.error(`Command download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.get("/api/commands/download-file",e=>{try{const t=(e.req.query("path")||"").trim();if(!t||t.includes(".."))return e.json({error:"Invalid path"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","commands",t);if(!d(n)||!l(n).isFile())return e.json({error:"File not found"},404);const o=a(n),s=v(n);return new Response(o,{headers:{"Content-Type":"application/octet-stream","Content-Disposition":`attachment; filename="${s}"`}})}catch(t){return H.error(`Command file download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.get("/api/commands/file",e=>{try{const t=(e.req.query("path")||"").trim();if(!t||t.includes(".."))return e.json({error:"Invalid path"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","commands",t);if(!d(n)||!l(n).isFile())return e.json({error:"File not found"},404);const o=a(n,"utf-8");return e.json({path:t,content:o})}catch(t){return H.error(`Command file read failed: ${t}`),e.json({error:"Read failed"},500)}}),t.post("/api/commands/file",async e=>{try{const t=await e.req.json(),r=(t.path||"").trim(),n=t.content;if(!r||r.includes(".."))return e.json({error:"Invalid path"},400);if("string"!=typeof n)return e.json({error:"Content is required"},400);const o=this.server.getConfig(),s=j(o.agent.workspacePath,".claude","commands",r),a=j(s,"..");return f(a,{recursive:!0}),i(s,n,"utf-8"),H.info(`Command file saved via Nostromo: ${r}`),e.json({ok:!0})}catch(t){return H.error(`Command file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.post("/api/commands/create",async e=>{try{const t=await e.req.json(),r=(t.name||"").trim();if(!r||!/^[a-zA-Z0-9_-]+$/.test(r))return e.json({error:"Invalid name. Use only letters, digits, hyphens, and underscores."},400);const n=(t.description||"").trim(),o=(t.model||"").trim(),s=this.server.getConfig(),a=j(s.agent.workspacePath,".claude","commands");f(a,{recursive:!0});const c=j(a,r+".md");if(d(c))return e.json({error:"A command with this name already exists"},409);let l="---\n";return l+=`name: ${r}\n`,n&&(l+=`description: ${n}\n`),o&&(l+=`model: ${o}\n`),l+="---\n\n",i(c,l,"utf-8"),H.info(`Standalone command created via Nostromo: ${r}.md`),e.json({ok:!0,file:r+".md"})}catch(t){return H.error(`Command creation failed: ${t}`),e.json({error:"Creation failed"},500)}}),t.get("/api/skills",e=>{const t=this.server.getConfig(),r=j(t.agent.workspacePath,".claude","skills"),n=[];if(!d(r))return e.json(n);try{const e=c(r);for(const t of e){const e=j(r,t);if(!l(e).isDirectory())continue;const o=j(e,"SKILL.md");let s=t,i="";if(d(o))try{const e=G(a(o,"utf-8"));e.name&&(s=e.name),e.description&&(i=e.description)}catch{}n.push({folder:t,name:s,description:i})}}catch(e){H.error(`Failed to list skills: ${e}`)}return e.json(n)}),t.post("/api/skills/delete",async e=>{try{const t=((await e.req.json()).folder||"").trim();if(!t||t.includes("..")||t.includes("/"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","skills",t);return d(n)?(l(n).isDirectory()?u(n,{recursive:!0,force:!0}):p(n),H.info(`Skill deleted via Nostromo: ${t}`),e.json({ok:!0})):e.json({error:"Skill not found"},404)}catch(t){return H.error(`Failed to delete skill: ${t}`),e.json({error:"Failed to delete skill"},500)}}),t.post("/api/skills/upload",async e=>{try{const t=this.server.getConfig(),r=await e.req.formData(),n=(r.get("folderName")||"").trim();if(!n)return e.json({error:"folderName is required"},400);if(n.includes("..")||n.includes("/")||n.includes("\\"))return e.json({error:"Invalid folder name"},400);const o=r.getAll("files"),s=r.getAll("paths");if(0===o.length)return e.json({error:"No files provided"},400);if(o.length!==s.length)return e.json({error:"files and paths count mismatch"},400);const a=j(t.agent.workspacePath,".claude","skills",n);f(a,{recursive:!0});for(let e=0;e<o.length;e++){const t=o[e],r=s[e];if(r.includes(".."))continue;const n=j(a,r),c=j(n,"..");f(c,{recursive:!0});const l=Buffer.from(await t.arrayBuffer());i(n,l)}return H.info(`Skill folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return H.error(`Skill upload failed: ${t}`),e.json({error:"Upload failed"},500)}}),t.get("/api/skills/download/:folder",e=>{try{const t=e.req.param("folder");if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","skills"),o=j(n,t);if(!d(o)||!l(o).isDirectory())return e.json({error:"Skill folder not found"},404);const s=h("zip",["-r","-q","-",t],{cwd:n,maxBuffer:104857600});return new Response(s,{headers:{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${t}.zip"`}})}catch(t){return H.error(`Skill download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.get("/api/skills/file",e=>{try{const t=(e.req.query("folder")||"").trim();if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=j(r.agent.workspacePath,".claude","skills",t,"SKILL.md");if(!d(n))return e.json({path:t+"/SKILL.md",content:""});const o=a(n,"utf-8");return e.json({path:t+"/SKILL.md",content:o})}catch(t){return H.error(`Skill file read failed: ${t}`),e.json({error:"Read failed"},500)}}),t.post("/api/skills/file",async e=>{try{const t=await e.req.json(),r=(t.folder||"").trim(),n=t.content;if(!r||r.includes("..")||r.includes("/")||r.includes("\\"))return e.json({error:"Invalid folder name"},400);if("string"!=typeof n)return e.json({error:"Content is required"},400);const o=this.server.getConfig(),s=j(o.agent.workspacePath,".claude","skills",r);f(s,{recursive:!0});const a=j(s,"SKILL.md");return i(a,n,"utf-8"),H.info(`Skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return H.error(`Skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/skills/bundled",e=>{try{const t=D();if(!t)return e.json([]);const r=c(t,{withFileTypes:!0}),n=[];for(const e of r){if(!e.isDirectory())continue;const r=j(t,e.name,"SKILL.md");if(!d(r))continue;const o=G(a(r,"utf-8"));n.push({folder:e.name,name:o.name||e.name,description:o.description||""})}return n.sort((e,t)=>e.name.localeCompare(t.name)),e.json(n)}catch(t){return H.error(`Failed to list bundled skills: ${t}`),e.json({error:"Failed to list bundled skills"},500)}}),t.post("/api/skills/use-bundled",async e=>{try{const t=((await e.req.json()).folder||"").trim();if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=D();if(!r)return e.json({error:"Bundled directory not found"},404);const n=j(r,t);if(!d(n))return e.json({error:"Bundled skill not found"},404);const o=this.server.getConfig(),s=j(o.agent.workspacePath,".claude","skills",t);return m(n,s,{recursive:!0}),H.info(`Bundled skill installed via Nostromo: ${t}`),e.json({ok:!0,name:t})}catch(t){return H.error(`Use bundled skill failed: ${t}`),e.json({error:"Install failed"},500)}}),t.get("/api/skills/bundled/file",e=>{try{const t=(e.req.query("folder")||"").trim();if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=D();if(!r)return e.json({error:"Bundled directory not found"},404);const n=j(r,t,"SKILL.md");if(!d(n))return e.json({error:"File not found"},404);const o=a(n,"utf-8");return e.json({content:o})}catch(t){return H.error(`Bundled skill file read failed: ${t}`),e.json({error:"Read failed"},500)}}),t.post("/api/skills/bundled/file",async e=>{try{const t=await e.req.json(),r=(t.folder||"").trim(),n=t.content;if(!r||r.includes("..")||r.includes("/")||r.includes("\\"))return e.json({error:"Invalid folder name"},400);if("string"!=typeof n)return e.json({error:"Content is required"},400);const o=D();if(!o)return e.json({error:"Bundled directory not found"},404);const s=j(o,r);if(!d(s))return e.json({error:"Bundled skill folder not found"},404);const a=j(s,"SKILL.md");return i(a,n,"utf-8"),H.info(`Bundled skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return H.error(`Bundled skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/prompt-simulate",e=>{try{const t=S(),r=q(t.dataDir),n={sessionKey:"simulate:preview",channel:"simulate",chatId:"preview",sessionId:"(simulated)",memoryFile:j(t.dataDir,"memory","simulate_preview","2026-01-01.md"),attachmentsDir:j(t.dataDir,"memory","simulate_preview","2026-01-01")},o=this.server.getAgentService().getToolServers(),s=T({config:t,sessionContext:n,workspaceFiles:r,mode:"full",hasNodeTools:this.server.getAgentService().hasNodeTools(),hasMessageTools:this.server.getAgentService().hasMessageTools(),coderSkill:t.agent.builtinCoderSkill,toolServers:o}),i=T({config:t,sessionContext:n,workspaceFiles:r,mode:"minimal",hasNodeTools:this.server.getAgentService().hasNodeTools(),hasMessageTools:this.server.getAgentService().hasMessageTools(),coderSkill:t.agent.builtinCoderSkill,subagentTask:"(example subagent task description)",toolServers:o});return e.json({main:s,subagent:i})}catch(t){return H.error(`Prompt simulation failed: ${t}`),e.json({error:"Failed to simulate prompt: "+String(t)},500)}}),t.post("/api/memory-search/test-embedding",async e=>{try{const t=await e.req.json(),r=t.modelRef||"",n=t.embeddingModel||"text-embedding-3-small",o=t.embeddingDimensions||1536,s=this.server.getConfig(),i=N(s,r),a=(i?.useEnvVar?process.env[i.useEnvVar]:i?.apiKey)||"",c=i?.baseURL||"";if(!a)return e.json({ok:!1,error:`No API key found for modelRef "${r}"`},400);const{default:l}=await import("openai"),d=new l({apiKey:a,...c?{baseURL:c}:{}}),u=Date.now(),f=await d.embeddings.create({model:n,input:"test embedding connection",dimensions:o}),p=Date.now()-u,m=f.data?.[0]?.embedding?.length??0;return e.json({ok:!0,model:n,dimensions:m,latencyMs:p})}catch(t){const r=t instanceof Error?t.message:String(t);return H.error(`Embedding test failed: ${r}`),e.json({ok:!1,error:r},500)}}),t.get("/api/memory-files",e=>{const t=this.server.getConfig().memoryDir;if(!d(t))return e.json([]);try{const r=[],n=c(t,{withFileTypes:!0});for(const e of n){if(!e.isDirectory())continue;const n=j(t,e.name),o=c(n).filter(e=>e.endsWith(".md")).map(e=>{const t=l(j(n,e));return{name:e,size:t.size,modified:t.mtime.toISOString()}}).sort((e,t)=>t.name.localeCompare(e.name));o.length>0&&r.push({sessionKey:e.name,files:o})}return r.sort((e,t)=>e.sessionKey.localeCompare(t.sessionKey)),e.json(r)}catch(t){return H.error(`Failed to list memory files: ${t}`),e.json([])}}),t.get("/api/memory-files/:sessionKey/:fileName",e=>{const t=e.req.param("sessionKey"),r=e.req.param("fileName");if(!/^[\w-]+\.md$/.test(r))return e.json({error:"Invalid file name"},400);if(/[/\\.]/.test(t.replace(/:/g,"")))return e.json({error:"Invalid session key"},400);const n=this.server.getConfig(),o=j(n.memoryDir,t.replace(/:/g,"_"),r);if(!d(o))return e.json({error:"File not found"},404);try{const n=a(o,"utf-8");return e.json({sessionKey:t,fileName:r,content:n})}catch(t){return e.json({error:"Failed to read file"},500)}}),t.get("/api/logs/current",e=>{const t=U();if(!t)return e.json({lines:[],total:0});const r=j(t,"gmab.log");try{const t=a(r,"utf-8").split("\n").filter(e=>e.length>0),n=Math.min(Math.max(parseInt(e.req.query("lines")||"200")||200,1),1e3),o=t.slice(-n);return e.json({lines:o,total:t.length})}catch{return e.json({lines:[],total:0})}}),t.get("/api/logs/current/download",async e=>{const t=U();if(!t)return e.json({error:"Logs not initialized"},503);const r=j(t,"gmab.log"),n="true"===e.req.query("compress");try{if(n){const e=a(r),{gzipSync:t}=await import("node:zlib"),n=t(e);return new Response(n,{headers:{"Content-Type":"application/gzip","Content-Disposition":'attachment; filename="gmab.log.gz"'}})}const e=a(r);return new Response(e,{headers:{"Content-Type":"text/plain","Content-Disposition":'attachment; filename="gmab.log"'}})}catch{return e.json({error:"Log file not found"},404)}}),t.get("/api/logs/files",e=>{const t=U();if(!t)return e.json([]);try{const r=c(t).filter(e=>/^gmab\.\d+\.log$/.test(e)).map(e=>{const r=l(j(t,e));return{name:e,size:r.size,modified:r.mtime.toISOString()}}).sort((e,t)=>e.name.localeCompare(t.name,void 0,{numeric:!0}));return e.json(r)}catch{return e.json([])}}),t.get("/api/logs/files/:name/download",async e=>{const t=U();if(!t)return e.json({error:"Logs not initialized"},503);const r=e.req.param("name");if(!/^gmab\.\d+\.log$/.test(r))return e.json({error:"Invalid file name"},400);const n=j(t,r);try{const e=a(n),{gzipSync:t}=await import("node:zlib"),o=t(e);return new Response(o,{headers:{"Content-Type":"application/gzip","Content-Disposition":`attachment; filename="${r}.gz"`}})}catch{return e.json({error:"File not found"},404)}}),t.get("/api/logs/level",e=>e.json({level:W()})),t.put("/api/logs/level",async e=>{try{const t=(await e.req.json()).level,r=["debug","info","warn","error"];if(!r.includes(t))return e.json({error:"Invalid level. Must be one of: "+r.join(", ")},400);J(t);try{const e=y(process.cwd(),"config.yaml"),{parse:r}=await import("yaml"),n=r(a(e,"utf-8"))||{};n.logLevel=t;const o=s(n);b(e),i(e,o,"utf-8")}catch(e){H.warn(`Failed to persist logLevel to config.yaml: ${e}`)}return H.info(`Log level changed to ${t}`),e.json({ok:!0,level:t})}catch(t){return e.json({error:"Failed to set log level"},400)}}),t.put("/api/logs/verbose",async e=>{try{const t=!!(await e.req.json()).enabled;try{const e=y(process.cwd(),"config.yaml"),{parse:r}=await import("yaml"),n=r(a(e,"utf-8"))||{};n.verboseDebugLogs=t;const o=s(n);b(e),i(e,o,"utf-8")}catch(e){H.warn(`Failed to persist verboseDebugLogs to config.yaml: ${e}`)}return H.info("Verbose debug logs "+(t?"enabled":"disabled")),e.json({ok:!0,enabled:t})}catch(t){return e.json({error:"Failed to set verbose logs"},400)}})}async start(){this.httpServer=t({fetch:this.app.fetch,hostname:this.host,port:this.port}),this.wss=new k({noServer:!0}),this.httpServer.on("upgrade",(e,t,r)=>{const n=new URL(e.url??"/",`http://localhost:${this.port}`),o="/"===this.basePath?"":this.basePath;n.pathname===o+"/ws/nodes"?this.wss.handleUpgrade(e,t,r,t=>{this.wss.emit("connection",t,e)}):t.destroy()}),this.wss.on("connection",e=>{let t=null,r=null,n=!1;const o=()=>{n&&t&&(this.nodeRegistry.unregister(t),n=!1),r&&this.pendingNodes.delete(r);const o=this.server.getWebChatChannel();o&&o.unregisterByWs(e)};e.on("message",o=>{try{const s=JSON.parse(o.toString());if("hello"===s.type){t=s.nodeId;const o=s.signature;if(!t)return void e.close(1008,"Missing nodeId in hello");if(!o)return void e.close(1008,"Missing signature");r=o;const i=this.server.getNodeSignatureDb(),a=i.createOrUpdatePending(t,o,s.hostname??"",s.displayName??"",s.platform??"",s.arch??""),c={displayName:s.displayName,platform:s.platform,arch:s.arch,hostname:s.hostname,capabilities:s.capabilities??[],commands:s.commands??[]};switch(a.status){case"pending":e.send(JSON.stringify({type:"pairing_status",status:"pending"})),this.pendingNodes.set(o,{ws:e,nodeId:t,info:c}),H.info(`Node ${t} (${s.displayName??"unnamed"}) awaiting approval`);break;case"approved":e.send(JSON.stringify({type:"pairing_status",status:"approved"})),this.nodeRegistry.register(t,e,c),n=!0,i.updateLastSeen(a.id),H.info(`Node ${t} (${s.displayName??"unnamed"}) approved and registered`);break;case"revoked":e.send(JSON.stringify({type:"pairing_status",status:"revoked"})),e.close(1008,"Signature revoked")}}else if("command_result"===s.type)this.nodeRegistry.handleCommandResult(s.id,{ok:s.ok,result:s.result,error:s.error});else if("chat"===s.type){if(!n||!t)return;const r=this.server.getWebChatChannel();if(!r)return;const o=this.nodeRegistry.getNode(t),i=$(o?.displayName??"node",t),a=s.chatId,c=a?`${i}/${a}`:i;r.registerConnection(c,e),r.handleNodeChat(c,t,{text:s.text,attachments:s.attachments})}else if("a2ui_action"===s.type){if(!n||!t)return;const e=this.server.getWebChatChannel();if(!e)return;const r=s.userAction;if(!r||!r.name)return;const o=this.nodeRegistry.getNode(t),i=$(o?.displayName??"node",t),a=s.chatId,c=a?`${i}/${a}`:i,l=r.context&&Object.keys(r.context).length>0?` ${JSON.stringify(r.context)}`:"",d=`[A2UI Action] ${r.name} on surface ${r.surfaceId||"main"} (component: ${r.sourceComponentId||"unknown"})${l}`;e.handleNodeChat(c,t,{text:d}),H.info(`A2UI action from node ${t}: ${r.name} on ${r.surfaceId||"main"}`)}else if("dynamic_ui_action"===s.type){if(!n||!t)return;const r=this.server.getWebChatChannel();if(!r)return;const o=s.action;if(!o||!o.activityId)return;const i=s.channel,a=s.chatId;let c;if(i&&a)c=a,r.registerConnection(a,e);else{const e=this.nodeRegistry.getNode(t);c=$(e?.displayName??"node",t)}const l=o.data?` data=${JSON.stringify(o.data)}`:"",d=o.context&&Object.keys(o.context).length>0?` context=${JSON.stringify(o.context)}`:"",u=`[Dynamic UI Action] ${o.type} on #${o.activityId}${l}${d}`;if(i&&"webchat"!==i&&a){const e=this.server.getChannelManager(),n=e?.getAdapter(i);n&&e?(e.setTyping(i,a).catch(()=>{}),this.server.handleMessage({chatId:a,userId:t,channelName:i,text:u,attachments:[]}).then(async t=>{t&&t.trim()&&await e.sendResponse(i,a,t)}).catch(e=>{H.error(`Error handling dynamic_ui_action for ${i}:${a}: ${e}`)})):r.handleNodeChat(c,t,{text:u})}else r.handleNodeChat(c,t,{text:u});H.info(`Dynamic UI action from node ${t}: ${o.type} on #${o.activityId} → ${i??"webchat"}:${c}`)}else"ping"===s.type&&e.send(JSON.stringify({type:"pong"}))}catch{}}),e.on("close",()=>{o()}),e.on("error",e=>{H.error(`Node WS error: ${e.message}`),o()})}),H.info(`Nostromo listening on http://${this.host}:${this.port}${this.basePath}`)}notifyPairingChange(e,t){const r=this.server.getNodeSignatureDb(),n=r.getById(e);if(!n)return;const o=n.signature;if("approved"===t){const t=this.pendingNodes.get(o);t&&(t.ws.send(JSON.stringify({type:"pairing_status",status:"approved"})),this.nodeRegistry.register(t.nodeId,t.ws,t.info),this.pendingNodes.delete(o),r.updateLastSeen(e),H.info(`Node ${t.nodeId} approved via Nostromo`))}else if("revoked"===t){const e=this.pendingNodes.get(o);if(e)return e.ws.send(JSON.stringify({type:"pairing_status",status:"revoked"})),e.ws.close(1008,"Signature revoked"),void this.pendingNodes.delete(o);const t=this.nodeRegistry.listNodes();for(const e of t){const t=this.nodeRegistry.getNode(e.nodeId);if(t&&e.nodeId===n.nodeId){t.ws.send(JSON.stringify({type:"pairing_status",status:"revoked"})),t.ws.close(1008,"Signature revoked"),this.nodeRegistry.unregister(e.nodeId);break}}}}async stop(){this.wss&&this.wss.close(),this.httpServer&&this.httpServer.close(),H.info("Nostromo stopped")}}function V(e){return"string"==typeof e&&/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(e)}const Z={telegram:[{field:"botToken",envSuffix:"BOT_TOKEN"}],discord:[{field:"token",envSuffix:"TOKEN"}],slack:[{field:"botToken",envSuffix:"BOT_TOKEN"},{field:"appToken",envSuffix:"APP_TOKEN"}],msteams:[{field:"appSecret",envSuffix:"APP_SECRET"}],line:[{field:"channelAccessToken",envSuffix:"CHANNEL_ACCESS_TOKEN"},{field:"channelSecret",envSuffix:"CHANNEL_SECRET"}],matrix:[{field:"accessToken",envSuffix:"ACCESS_TOKEN"}]};function G(e){const t=e.match(/^---\r?\n([\s\S]*?)\r?\n---/);if(!t)return{};const r={};for(const e of t[1].split("\n")){const t=e.indexOf(":");if(t<0)continue;const n=e.slice(0,t).trim(),o=e.slice(t+1).trim().replace(/^["']|["']$/g,"");n&&(r[n]=o)}return r}
|
|
1
|
+
import{Hono as e}from"hono";import{serve as t}from"@hono/node-server";import{getCookie as r,setCookie as n,deleteCookie as o}from"hono/cookie";import{stringify as s}from"yaml";import{writeFileSync as i,readFileSync as a,readdirSync as c,statSync as l,existsSync as d,rmSync as u,mkdirSync as f,unlinkSync as p,cpSync as m}from"node:fs";import{execFileSync as h}from"node:child_process";import{createHash as g}from"node:crypto";import{resolve as y,join as v,basename as j}from"node:path";import{WebSocketServer as k}from"ws";import{isHeartbeatContentEffectivelyEmpty as w}from"../cron/heartbeat-token.js";import{buildWebChatId as S}from"../gateway/channels/webchat.js";import{loadConfig as $,loadRawConfig as C,backupConfig as b,resolveModelEntry as N}from"../config.js";import{renderSPA as I}from"./ui.js";import{buildSystemPrompt as T}from"../agent/prompt-builder.js";import{loadWorkspaceFiles as q}from"../agent/workspace-files.js";import{resolveBundledDir as D}from"../utils/package-paths.js";import{readKey as P,isDefaultKey as A,validateKey as x,regenerateKey as O,createSession as E,validateSession as R,validateCsrf as K,getCsrfToken as _,destroySession as F,checkRateLimit as L,recordLoginFailure as B,resetLoginAttempts as M}from"./auth.js";import{createLogger as z,getLogsDir as U,getLogLevel as W,setLogLevel as J}from"../utils/logger.js";const H=z("Nostromo"),Y="nostromo_session";export class Nostromo{app;httpServer=null;wss=null;host;port;server;nodeRegistry;startedAt;pendingNodes=new Map;configHash="";configPath;basePath;constructor(t,r,n,o,s="/nostromo"){this.server=t,this.host=r,this.port=n,this.nodeRegistry=o,this.basePath=s.replace(/\/+$/,"")||"/",this.startedAt=Date.now(),this.app=new e,this.configPath=y(process.cwd(),"config.yaml"),this.configHash=this.hashConfigFile(),P(),this.setupRoutes()}hashConfigFile(){try{const e=a(this.configPath,"utf-8");return g("sha256").update(e).digest("hex")}catch{return""}}setupRoutes(){const e="/"===this.basePath?"":this.basePath,t=e?this.app.basePath(e):this.app;e&&this.app.get("/",t=>t.redirect(e+"/")),t.use("*",async(t,r)=>{const n=Date.now();await r();const o=Date.now()-n;t.req.path===e+"/api/logs/current"||t.req.path===e+"/api/config/check"||t.req.path===e+"/api/config"||t.req.path===e+"/api/memory-files"||t.req.path===e+"/api/status"||t.req.path===e+"/api/auth/session"?H.debug(`${t.req.method} ${t.req.path} ${t.res.status} ${o}ms`):H.info(`${t.req.method} ${t.req.path} ${t.res.status} ${o}ms`)}),t.use("/api/*",async(e,t)=>{const r=e.req.header("Origin");return r&&(e.header("Access-Control-Allow-Origin",r),e.header("Access-Control-Allow-Credentials","true"),e.header("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS"),e.header("Access-Control-Allow-Headers","Content-Type, Authorization, X-CSRF-Token")),"OPTIONS"===e.req.method?new Response(null,{status:204}):t()}),t.get("/",t=>t.html(I(A(),e))),t.post("/api/auth/login",async t=>{const r=t.req.header("x-forwarded-for")||t.req.header("x-real-ip")||"unknown",o=L(r);if(!o.allowed)return t.json({error:`Too many attempts. Try again in ${o.retryAfter}s`},429);const s=(await t.req.json()).key;if(!x(s))return B(r),t.json({error:"Invalid access key"},401);M(r);const{sessionToken:i,csrfToken:a}=E();if(n(t,Y,i,{httpOnly:!0,sameSite:"Lax",path:e||"/",maxAge:86400}),A()){const e=O();return H.info("Default key used; rotated to new key"),t.json({ok:!0,newKey:e,csrfToken:a})}return t.json({ok:!0,csrfToken:a})}),t.post("/api/auth/logout",async t=>{const n=r(t,Y);return n&&F(n),o(t,Y,{path:e||"/"}),t.json({ok:!0})}),t.get("/api/auth/session",e=>{const t=r(e,Y);if(!R(t))return e.json({valid:!1},401);const n=_(t);return e.json({valid:!0,csrfToken:n})}),t.use("/api/*",async(t,n)=>{if(t.req.path.startsWith(e+"/api/auth/"))return n();const o=t.req.header("Authorization");if(o?.startsWith("Bearer ")){const e=o.slice(7);return this.server.getTokenDb().validateToken(e,"nostromo")?n():t.json({error:"Invalid API token"},401)}const s=r(t,Y);if(!R(s))return t.json({error:"Unauthorized"},401);if("POST"===t.req.method||"PUT"===t.req.method||"DELETE"===t.req.method){const e=t.req.header("X-CSRF-Token");if(!K(s,e))return t.json({error:"Invalid CSRF token"},403)}return n()}),t.get("/api/config",e=>{const t=C(),r=this.server.getConfig();return t.dataDir=r.dataDir,t.gmabPath=r.gmabPath,e.json(t)}),t.put("/api/config",async e=>{try{const t=await e.req.json();if(t.cron?.heartbeat?.enabled&&(!t.cron.heartbeat.message||t.cron.heartbeat.message.trim().length<15))return e.json({error:"Heartbeat message is required and must be at least 15 characters"},400);const{dataDir:r,dbPath:n,memoryDir:o,cronStorePath:c,...l}=t,u=function(e){const t={};if(e.channels)for(const[r,n]of Object.entries(Z)){const o=e.channels[r];if(o?.accounts)for(const[e,s]of Object.entries(o.accounts))for(const o of n){const n=s[o.field];if("string"==typeof n&&n.length>0&&!V(n)){const i=`${r.toUpperCase()}_${e.toUpperCase().replace(/-/g,"_")}_${o.envSuffix}`;t[i]=n,s[o.field]=`\${${i}}`}}}if(Array.isArray(e.models))for(const r of e.models){if((r.types||["external"]).includes("internal"))continue;const e=r.apiKey;if("string"==typeof e&&e.length>0&&!V(e)){const n=r.useEnvVar||"OPENAI_API_KEY";t[n]=e,r.apiKey=`\${${n}}`,r.useEnvVar||(r.useEnvVar=n)}const n=r.fastProxyApiKey;if("string"==typeof n&&n.length>0&&!V(n)){const e="FAST_PROXY_APIKEY";t[e]=n,r.fastProxyApiKey=`\${${e}}`}}if(e.stt?.["openai-whisper"]?.apiKey){const r=e.stt["openai-whisper"].apiKey;if("string"==typeof r&&r.length>0&&!V(r)){const n="STT_OPENAI_WHISPER_API_KEY";t[n]=r,e.stt["openai-whisper"].apiKey=`\${${n}}`}}if(Object.keys(t).length>0){!function(e){const t=y(process.cwd(),".env");let r=d(t)?a(t,"utf-8"):"";for(const[t,n]of Object.entries(e)){const e=new RegExp(`^${t}=.*$`,"m");e.test(r)?r=r.replace(e,`${t}=${n}`):(r.length>0&&!r.endsWith("\n")&&(r+="\n"),r+=`${t}=${n}\n`)}i(t,r,"utf-8")}(t);for(const[e,r]of Object.entries(t))process.env[e]=r;H.info(`Protected ${Object.keys(t).length} secret(s) → .env`)}return e}(l),f=s(u),p=y(process.cwd(),"config.yaml");return b(p),i(p,f,"utf-8"),H.info("Config updated via Nostromo"),e.json({ok:!0})}catch(t){return H.error(`Config save failed: ${t}`),e.json({error:"Failed to save configuration"},500)}}),t.get("/api/tokens",e=>{const t=this.server.getTokenDb().listTokens();return e.json(t)}),t.post("/api/tokens",async e=>{const t=await e.req.json(),r=this.server.getTokenDb().createToken({userId:t.userId,channel:t.channel,label:t.label});return e.json(r,201)}),t.delete("/api/tokens/:id",e=>{const t=parseInt(e.req.param("id"));return this.server.getTokenDb().deleteToken(t),e.json({ok:!0})}),t.post("/api/tokens/:id/revoke",e=>{const t=parseInt(e.req.param("id"));return this.server.getTokenDb().revokeToken(t),e.json({ok:!0})}),t.post("/api/tokens/:id/approve",e=>{const t=parseInt(e.req.param("id"));return this.server.getTokenDb().approveToken(t),e.json({ok:!0})}),t.get("/api/key",e=>e.json({key:P()})),t.post("/api/key/regenerate",e=>{const t=O();return e.json({key:t})}),t.get("/api/whatsapp/qr",e=>{const t=this.server.getWhatsAppQrState();return e.json(t)}),t.get("/api/status",e=>{const t=Date.now()-this.startedAt,r=Math.floor(t/1e3),n=`${Math.floor(r/3600)}h ${Math.floor(r%3600/60)}m ${r%60}s`;return e.json({status:"online",uptime:n,uptimeMs:t})}),t.post("/api/server/restart",async e=>{try{H.info("Server restart requested via Nostromo");const t=$();return await this.server.reconfigure(t),this.startedAt=Date.now(),this.configHash=this.hashConfigFile(),H.info("Server restarted successfully via Nostromo"),e.json({ok:!0})}catch(t){return H.error(`Server restart failed: ${t}`),e.json({error:`Restart failed: ${t instanceof Error?t.message:String(t)}`},500)}}),t.get("/api/config/check",e=>{const t=this.hashConfigFile(),r=t!==this.configHash&&""!==t;return e.json({restartNeeded:r})}),t.get("/api/sessions",e=>{const t=this.server.getSessionDb().listSessions(),r=this.server.getAgentService(),n=new Set(r.getActiveSessions()),o=t.map(e=>{const[t,...o]=e.sessionKey.split(":"),s=o.join(":"),i=new Date(e.createdAt+"Z"),a=e=>String(e).padStart(2,"0"),c=`${i.getFullYear()}${a(i.getMonth()+1)}${a(i.getDate())}_${a(i.getHours())}${a(i.getMinutes())}`,l=`${e.sdkSessionId??"pending"}:${s}:${t}:${c}`;return{sessionKey:e.sessionKey,channelName:t,chatId:s,sdkSessionId:e.sdkSessionId,modelOverride:e.modelOverride,createdAt:e.createdAt,lastActivity:e.lastActivity,displayId:l,active:n.has(e.sessionKey),busy:r.isBusy(e.sessionKey)}});return e.json(o)}),t.post("/api/sessions/:sessionKey/messages",async e=>{try{const t=e.req.param("sessionKey"),r=(await e.req.json()).message;if(!r)return e.json({error:"message is required"},400);const n=this.server.getSessionDb();if(!n.getBySessionKey(t))return e.json({error:"Session not found"},404);const[o,...s]=t.split(":"),i=s.join(":"),a={chatId:i,userId:"nostromo",channelName:o,text:r,attachments:[]},c=await this.server.handleMessage(a),l=this.server.getChannelManager();return await l.sendToChannel(o,i,c),e.json({ok:!0,sessionKey:t,response:c})}catch(t){return H.error(`Cross-session message failed: ${t}`),e.json({error:"Failed to process message"},500)}}),t.get("/api/nodes",e=>{const t=this.nodeRegistry.listNodes();return e.json(t)}),t.get("/api/nodes/signatures",e=>{const t=this.server.getNodeSignatureDb(),r=e.req.query("status"),n=r?t.listByStatus(r):t.listAll();return e.json(n)}),t.post("/api/nodes/signatures/:id/approve",e=>{const t=parseInt(e.req.param("id")),r=this.server.getNodeSignatureDb();return r.getById(t)?(r.approve(t),this.notifyPairingChange(t,"approved"),e.json({ok:!0})):e.json({error:"Not found"},404)}),t.post("/api/nodes/signatures/:id/revoke",e=>{const t=parseInt(e.req.param("id")),r=this.server.getNodeSignatureDb();return r.getById(t)?(r.revoke(t),this.notifyPairingChange(t,"revoked"),e.json({ok:!0})):e.json({error:"Not found"},404)}),t.delete("/api/nodes/signatures/:id",e=>{const t=parseInt(e.req.param("id")),r=this.server.getNodeSignatureDb();return r.getById(t)?(this.notifyPairingChange(t,"revoked"),r.delete(t),e.json({ok:!0})):e.json({error:"Not found"},404)}),t.get("/api/cron/status",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);const r=await t.status();return e.json(r)}),t.get("/api/cron/jobs",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);const r="true"===e.req.query("includeDisabled"),n=await t.list({includeDisabled:r});return e.json(n)}),t.post("/api/cron/jobs",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=await e.req.json(),n=await t.add(r);return e.json(n,201)}catch(t){return H.error(`Cron job add failed: ${t}`),e.json({error:String(t)},400)}}),t.put("/api/cron/jobs/:id",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=e.req.param("id"),n=await e.req.json(),o=await t.update(r,n);return e.json(o)}catch(t){return H.error(`Cron job update failed: ${t}`),e.json({error:String(t)},400)}}),t.delete("/api/cron/jobs/:id",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=e.req.param("id"),n=await t.remove(r);return e.json(n)}catch(t){return H.error(`Cron job remove failed: ${t}`),e.json({error:String(t)},400)}}),t.post("/api/cron/jobs/:id/run",async e=>{const t=this.server.getCronService();if(!t)return e.json({error:"Cron not available"},503);try{const r=e.req.param("id"),n="due"===(await e.req.json().catch(()=>({}))).mode?"due":"force",o=await t.run(r,n);return e.json(o)}catch(t){return H.error(`Cron job run failed: ${t}`),e.json({error:String(t)},400)}}),t.post("/api/heartbeat/simulate",async e=>{const t=this.server.getConfig(),r=v(t.dataDir,"HEARTBEAT.md");let n="",o=!1;try{d(r)&&(o=!0,n=a(r,"utf-8"))}catch{}const s=!(o&&w(n));return e.json({accepted:s,fileExists:o,content:n})}),t.post("/api/inject",async e=>{try{const t=await e.req.json(),{channel:r,chatId:n,text:o}=t;if(!r||!n||!o)return e.json({error:"channel, chatId, and text are required"},400);const s={chatId:n,userId:"inject",channelName:r,text:o,attachments:[]},i=await this.server.handleMessage(s),a=this.server.getChannelManager();await a.sendToChannel(r,n,i);const c=`${r}:${n}`;return e.json({ok:!0,sessionKey:c,response:i})}catch(t){return H.error(`Inject message failed: ${t}`),e.json({error:"Failed to inject message"},500)}}),t.get("/api/internal-tools",e=>{const t=this.server.getAgentService().getToolServers(),r=[];for(const e of t){const t=e,n=t.name||"unknown",o=t.instance?._registeredTools;if(!o)continue;const s=[];for(const[e,t]of Object.entries(o)){const r=[],n=t.inputSchema?.def?.shape;if(n)for(const[e,t]of Object.entries(n)){let n=t.type||"unknown";const o=t.isOptional?.()??!1;"optional"===n&&t.def?.innerType&&(n=t.def.innerType.type||"unknown","enum"===n&&t.def.innerType.def?.entries&&(n=Object.keys(t.def.innerType.def.entries).join("|"))),"enum"===n&&t.def?.entries&&(n=Object.keys(t.def.entries).join("|")),r.push({name:e,type:n,required:!o,description:t.description||""})}s.push({name:e,description:t.description||"",params:r})}r.push({server:n,tools:s})}return e.json(r)});const g=["AGENTS.md","SOUL.md","TOOLS.md","IDENTITY.md","USER.md","HEARTBEAT.md","BOOTSTRAP.md","MEMORY.md"],k=["SYSTEM_PROMPT.md","SYSTEM_PROMPT_SUBAGENT.md","CBINT.json"];t.get("/api/workspace-files",e=>{const t=this.server.getConfig().dataDir,r=[];for(const e of g){const n=v(t,e);if(d(n))try{r.push({name:e,content:a(n,"utf-8"),exists:!0,isTemplate:!1})}catch{r.push({name:e,content:"",exists:!1,isTemplate:!1})}else r.push({name:e,content:"",exists:!1,isTemplate:!1})}for(const e of k){const n=v(t,".templates",e);if(d(n))try{r.push({name:e,content:a(n,"utf-8"),exists:!0,isTemplate:!0})}catch{r.push({name:e,content:"",exists:!1,isTemplate:!0})}else r.push({name:e,content:"",exists:!1,isTemplate:!0})}return e.json(r)}),t.put("/api/workspace-files/:name",async e=>{const t=e.req.param("name"),r=await e.req.json(),n=r.content,o=!0===r.isTemplate;if(null==n)return e.json({error:"content is required"},400);const s=this.server.getConfig().dataDir;let a;if(o){if(!k.includes(t))return e.json({error:"Invalid template name"},400);a=v(s,".templates",t)}else{if(!g.includes(t))return e.json({error:"Invalid file name"},400);a=v(s,t)}try{return i(a,n,"utf-8"),H.info(`Workspace file saved via Nostromo: ${a}`),e.json({ok:!0})}catch(t){return H.error(`Failed to save workspace file: ${t}`),e.json({error:"Failed to save file"},500)}}),t.post("/api/plugins/upload",async e=>{try{const t=this.server.getConfig(),r=await e.req.formData(),n=(r.get("folderName")||"").trim();if(!n)return e.json({error:"folderName is required"},400);if(n.includes("..")||n.includes("/")||n.includes("\\"))return e.json({error:"Invalid folder name"},400);const o=r.getAll("files"),s=r.getAll("paths");if(0===o.length)return e.json({error:"No files provided"},400);if(o.length!==s.length)return e.json({error:"files and paths count mismatch"},400);const c=(r.get("basePath")||"").trim()||v(t.agent.workspacePath,".plugins"),l=v(c,n);f(l,{recursive:!0});for(let e=0;e<o.length;e++){const t=o[e],r=s[e];if(r.includes(".."))continue;const n=v(l,r),a=v(n,"..");f(a,{recursive:!0});const c=Buffer.from(await t.arrayBuffer());i(n,c)}let u=n,p=n;const m=v(l,".claude-plugin","plugin.json");if(d(m))try{const e=a(m,"utf-8"),t=JSON.parse(e);t.name&&(u=t.name),t.description&&(p=t.description)}catch{}return e.json({ok:!0,path:l,name:u,description:p})}catch(t){return H.error(`Plugin upload failed: ${t}`),e.json({error:"Upload failed"},500)}}),t.post("/api/plugins/delete-folder",async e=>{try{const t=((await e.req.json()).path||"").trim();if(!t)return e.json({error:"path is required"},400);const r=y(t);return d(r)?l(r).isDirectory()?(u(r,{recursive:!0,force:!0}),H.info(`Plugin folder deleted via Nostromo: ${r}`),e.json({ok:!0})):e.json({error:"Path is not a directory"},400):e.json({ok:!0,message:"Directory already gone"})}catch(t){return H.error(`Plugin folder delete failed: ${t}`),e.json({error:"Failed to delete folder"},500)}}),t.get("/api/plugins/download/:index",e=>{try{const t=parseInt(e.req.param("index"),10),r=this.server.getConfig(),n=r.agent?.plugins||[];if(isNaN(t)||t<0||t>=n.length)return e.json({error:"Invalid plugin index"},400);const o=n[t].path;if(!o)return e.json({error:"Plugin has no path"},400);const s=y(o);if(!d(s)||!l(s).isDirectory())return e.json({error:"Plugin folder not found"},404);const i=j(s),a=v(s,".."),c=h("zip",["-r","-q","-",i],{cwd:a,maxBuffer:104857600});return new Response(c,{headers:{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${i}.zip"`}})}catch(t){return H.error(`Plugin download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.post("/api/plugins/verify-path",async e=>{try{const t=((await e.req.json()).path||"").trim();if(!t)return e.json({ok:!1,error:"Path is empty"});const r=y(t);return d(r)?l(r).isDirectory()?e.json({ok:!0,path:r}):e.json({ok:!1,error:"Path is not a directory"}):e.json({ok:!1,error:"Directory does not exist"})}catch(t){return e.json({ok:!1,error:"Invalid path"})}}),t.get("/api/plugins/info",e=>{const t=this.server.getConfig().agent.plugins||[],r=[];for(let e=0;e<t.length;e++){const n=t[e];let o=n.name,s=n.description||"",i=!1;try{if(d(n.path)&&l(n.path).isDirectory()){i=c(n.path).length>0;const e=v(n.path,".claude-plugin","plugin.json");if(d(e))try{const t=a(e,"utf-8"),r=JSON.parse(t);r.name&&(o=r.name),r.description&&!s&&(s=r.description)}catch{}}}catch{}r.push({index:e,name:o,description:s,valid:i})}return e.json(r)}),t.get("/api/commands",e=>{const t=this.server.getConfig(),r=v(t.agent.workspacePath,".claude","commands"),n=[];if(!d(r))return e.json(n);const o=(e,t)=>{try{const r=c(e);for(const s of r){const r=v(e,s);if(l(r).isDirectory())o(r,t?`${t}/${s}`:s);else if(s.endsWith(".md")){const e=t?`${t}/${s}`:s;let o=s.replace(/\.md$/,""),i="",c="";try{const e=G(a(r,"utf-8"));e.name&&(o=e.name),e.description&&(i=e.description),e.model&&(c=e.model)}catch{}n.push({file:e,folder:t,name:o,description:i,model:c})}}}catch{}};return o(r,""),n.sort((e,t)=>e.folder!==t.folder?e.folder.localeCompare(t.folder):e.name.localeCompare(t.name)),e.json(n)}),t.post("/api/commands/delete",async e=>{try{const t=((await e.req.json()).path||"").trim();if(!t||t.includes(".."))return e.json({error:"Invalid path"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","commands",t);if(!d(n))return e.json({error:"Command not found"},404);if(l(n).isDirectory())u(n,{recursive:!0,force:!0});else{p(n);const e=v(n,"..");if(e!==v(r.agent.workspacePath,".claude","commands"))try{0===c(e).length&&u(e,{recursive:!0,force:!0})}catch{}}return H.info(`Command deleted via Nostromo: ${t}`),e.json({ok:!0})}catch(t){return H.error(`Failed to delete command: ${t}`),e.json({error:"Failed to delete command"},500)}}),t.post("/api/commands/upload",async e=>{try{const t=this.server.getConfig(),r=await e.req.formData(),n=(r.get("folderName")||"").trim();if(!n)return e.json({error:"folderName is required"},400);if(n.includes("..")||n.includes("/")||n.includes("\\"))return e.json({error:"Invalid folder name"},400);const o=r.getAll("files"),s=r.getAll("paths");if(0===o.length)return e.json({error:"No files provided"},400);if(o.length!==s.length)return e.json({error:"files and paths count mismatch"},400);const a=v(t.agent.workspacePath,".claude","commands",n);f(a,{recursive:!0});for(let e=0;e<o.length;e++){const t=o[e],r=s[e];if(r.includes(".."))continue;const n=v(a,r),c=v(n,"..");f(c,{recursive:!0});const l=Buffer.from(await t.arrayBuffer());i(n,l)}return H.info(`Command folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return H.error(`Command upload failed: ${t}`),e.json({error:"Upload failed"},500)}}),t.get("/api/commands/download/:folder",e=>{try{const t=e.req.param("folder");if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","commands"),o=v(n,t);if(!d(o)||!l(o).isDirectory())return e.json({error:"Command folder not found"},404);const s=h("zip",["-r","-q","-",t],{cwd:n,maxBuffer:104857600});return new Response(s,{headers:{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${t}.zip"`}})}catch(t){return H.error(`Command download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.get("/api/commands/download-file",e=>{try{const t=(e.req.query("path")||"").trim();if(!t||t.includes(".."))return e.json({error:"Invalid path"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","commands",t);if(!d(n)||!l(n).isFile())return e.json({error:"File not found"},404);const o=a(n),s=j(n);return new Response(o,{headers:{"Content-Type":"application/octet-stream","Content-Disposition":`attachment; filename="${s}"`}})}catch(t){return H.error(`Command file download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.get("/api/commands/file",e=>{try{const t=(e.req.query("path")||"").trim();if(!t||t.includes(".."))return e.json({error:"Invalid path"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","commands",t);if(!d(n)||!l(n).isFile())return e.json({error:"File not found"},404);const o=a(n,"utf-8");return e.json({path:t,content:o})}catch(t){return H.error(`Command file read failed: ${t}`),e.json({error:"Read failed"},500)}}),t.post("/api/commands/file",async e=>{try{const t=await e.req.json(),r=(t.path||"").trim(),n=t.content;if(!r||r.includes(".."))return e.json({error:"Invalid path"},400);if("string"!=typeof n)return e.json({error:"Content is required"},400);const o=this.server.getConfig(),s=v(o.agent.workspacePath,".claude","commands",r),a=v(s,"..");return f(a,{recursive:!0}),i(s,n,"utf-8"),H.info(`Command file saved via Nostromo: ${r}`),e.json({ok:!0})}catch(t){return H.error(`Command file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.post("/api/commands/create",async e=>{try{const t=await e.req.json(),r=(t.name||"").trim();if(!r||!/^[a-zA-Z0-9_-]+$/.test(r))return e.json({error:"Invalid name. Use only letters, digits, hyphens, and underscores."},400);const n=(t.description||"").trim(),o=(t.model||"").trim(),s=this.server.getConfig(),a=v(s.agent.workspacePath,".claude","commands");f(a,{recursive:!0});const c=v(a,r+".md");if(d(c))return e.json({error:"A command with this name already exists"},409);let l="---\n";return l+=`name: ${r}\n`,n&&(l+=`description: ${n}\n`),o&&(l+=`model: ${o}\n`),l+="---\n\n",i(c,l,"utf-8"),H.info(`Standalone command created via Nostromo: ${r}.md`),e.json({ok:!0,file:r+".md"})}catch(t){return H.error(`Command creation failed: ${t}`),e.json({error:"Creation failed"},500)}}),t.get("/api/skills",e=>{const t=this.server.getConfig(),r=v(t.agent.workspacePath,".claude","skills"),n=[];if(!d(r))return e.json(n);try{const e=c(r);for(const t of e){const e=v(r,t);if(!l(e).isDirectory())continue;const o=v(e,"SKILL.md");let s=t,i="";if(d(o))try{const e=G(a(o,"utf-8"));e.name&&(s=e.name),e.description&&(i=e.description)}catch{}n.push({folder:t,name:s,description:i})}}catch(e){H.error(`Failed to list skills: ${e}`)}return e.json(n)}),t.post("/api/skills/delete",async e=>{try{const t=((await e.req.json()).folder||"").trim();if(!t||t.includes("..")||t.includes("/"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","skills",t);return d(n)?(l(n).isDirectory()?u(n,{recursive:!0,force:!0}):p(n),H.info(`Skill deleted via Nostromo: ${t}`),e.json({ok:!0})):e.json({error:"Skill not found"},404)}catch(t){return H.error(`Failed to delete skill: ${t}`),e.json({error:"Failed to delete skill"},500)}}),t.post("/api/skills/upload",async e=>{try{const t=this.server.getConfig(),r=await e.req.formData(),n=(r.get("folderName")||"").trim();if(!n)return e.json({error:"folderName is required"},400);if(n.includes("..")||n.includes("/")||n.includes("\\"))return e.json({error:"Invalid folder name"},400);const o=r.getAll("files"),s=r.getAll("paths");if(0===o.length)return e.json({error:"No files provided"},400);if(o.length!==s.length)return e.json({error:"files and paths count mismatch"},400);const a=v(t.agent.workspacePath,".claude","skills",n);f(a,{recursive:!0});for(let e=0;e<o.length;e++){const t=o[e],r=s[e];if(r.includes(".."))continue;const n=v(a,r),c=v(n,"..");f(c,{recursive:!0});const l=Buffer.from(await t.arrayBuffer());i(n,l)}return H.info(`Skill folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return H.error(`Skill upload failed: ${t}`),e.json({error:"Upload failed"},500)}}),t.get("/api/skills/download/:folder",e=>{try{const t=e.req.param("folder");if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","skills"),o=v(n,t);if(!d(o)||!l(o).isDirectory())return e.json({error:"Skill folder not found"},404);const s=h("zip",["-r","-q","-",t],{cwd:n,maxBuffer:104857600});return new Response(s,{headers:{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${t}.zip"`}})}catch(t){return H.error(`Skill download failed: ${t}`),e.json({error:"Download failed"},500)}}),t.get("/api/skills/file",e=>{try{const t=(e.req.query("folder")||"").trim();if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=this.server.getConfig(),n=v(r.agent.workspacePath,".claude","skills",t,"SKILL.md");if(!d(n))return e.json({path:t+"/SKILL.md",content:""});const o=a(n,"utf-8");return e.json({path:t+"/SKILL.md",content:o})}catch(t){return H.error(`Skill file read failed: ${t}`),e.json({error:"Read failed"},500)}}),t.post("/api/skills/file",async e=>{try{const t=await e.req.json(),r=(t.folder||"").trim(),n=t.content;if(!r||r.includes("..")||r.includes("/")||r.includes("\\"))return e.json({error:"Invalid folder name"},400);if("string"!=typeof n)return e.json({error:"Content is required"},400);const o=this.server.getConfig(),s=v(o.agent.workspacePath,".claude","skills",r);f(s,{recursive:!0});const a=v(s,"SKILL.md");return i(a,n,"utf-8"),H.info(`Skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return H.error(`Skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/skills/bundled",e=>{try{const t=D();if(!t)return e.json([]);const r=c(t,{withFileTypes:!0}),n=[];for(const e of r){if(!e.isDirectory())continue;const r=v(t,e.name,"SKILL.md");if(!d(r))continue;const o=G(a(r,"utf-8"));n.push({folder:e.name,name:o.name||e.name,description:o.description||""})}return n.sort((e,t)=>e.name.localeCompare(t.name)),e.json(n)}catch(t){return H.error(`Failed to list bundled skills: ${t}`),e.json({error:"Failed to list bundled skills"},500)}}),t.post("/api/skills/use-bundled",async e=>{try{const t=((await e.req.json()).folder||"").trim();if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=D();if(!r)return e.json({error:"Bundled directory not found"},404);const n=v(r,t);if(!d(n))return e.json({error:"Bundled skill not found"},404);const o=this.server.getConfig(),s=v(o.agent.workspacePath,".claude","skills",t);return m(n,s,{recursive:!0}),H.info(`Bundled skill installed via Nostromo: ${t}`),e.json({ok:!0,name:t})}catch(t){return H.error(`Use bundled skill failed: ${t}`),e.json({error:"Install failed"},500)}}),t.get("/api/skills/bundled/file",e=>{try{const t=(e.req.query("folder")||"").trim();if(!t||t.includes("..")||t.includes("/")||t.includes("\\"))return e.json({error:"Invalid folder name"},400);const r=D();if(!r)return e.json({error:"Bundled directory not found"},404);const n=v(r,t,"SKILL.md");if(!d(n))return e.json({error:"File not found"},404);const o=a(n,"utf-8");return e.json({content:o})}catch(t){return H.error(`Bundled skill file read failed: ${t}`),e.json({error:"Read failed"},500)}}),t.post("/api/skills/bundled/file",async e=>{try{const t=await e.req.json(),r=(t.folder||"").trim(),n=t.content;if(!r||r.includes("..")||r.includes("/")||r.includes("\\"))return e.json({error:"Invalid folder name"},400);if("string"!=typeof n)return e.json({error:"Content is required"},400);const o=D();if(!o)return e.json({error:"Bundled directory not found"},404);const s=v(o,r);if(!d(s))return e.json({error:"Bundled skill folder not found"},404);const a=v(s,"SKILL.md");return i(a,n,"utf-8"),H.info(`Bundled skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return H.error(`Bundled skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/prompt-simulate",e=>{try{const t=$(),r=q(t.dataDir),n={sessionKey:"simulate:preview",channel:"simulate",chatId:"preview",sessionId:"(simulated)",memoryFile:v(t.dataDir,"memory","simulate_preview","2026-01-01.md"),attachmentsDir:v(t.dataDir,"memory","simulate_preview","2026-01-01")},o=this.server.getAgentService().getToolServers(),s=T({config:t,sessionContext:n,workspaceFiles:r,mode:"full",hasNodeTools:this.server.getAgentService().hasNodeTools(),hasMessageTools:this.server.getAgentService().hasMessageTools(),coderSkill:t.agent.builtinCoderSkill,toolServers:o}),i=T({config:t,sessionContext:n,workspaceFiles:r,mode:"minimal",hasNodeTools:this.server.getAgentService().hasNodeTools(),hasMessageTools:this.server.getAgentService().hasMessageTools(),coderSkill:t.agent.builtinCoderSkill,subagentTask:"(example subagent task description)",toolServers:o});return e.json({main:s,subagent:i})}catch(t){return H.error(`Prompt simulation failed: ${t}`),e.json({error:"Failed to simulate prompt: "+String(t)},500)}}),t.post("/api/memory-search/test-embedding",async e=>{try{const t=await e.req.json(),r=t.modelRef||"",n=t.embeddingModel||"text-embedding-3-small",o=t.embeddingDimensions||1536,s=this.server.getConfig(),i=N(s,r),a=(i?.useEnvVar?process.env[i.useEnvVar]:i?.apiKey)||"",c=i?.baseURL||"";if(!a)return e.json({ok:!1,error:`No API key found for modelRef "${r}"`},400);const{default:l}=await import("openai"),d=new l({apiKey:a,...c?{baseURL:c}:{}}),u=Date.now(),f=await d.embeddings.create({model:n,input:"test embedding connection",dimensions:o}),p=Date.now()-u,m=f.data?.[0]?.embedding?.length??0;return e.json({ok:!0,model:n,dimensions:m,latencyMs:p})}catch(t){const r=t instanceof Error?t.message:String(t);return H.error(`Embedding test failed: ${r}`),e.json({ok:!1,error:r},500)}}),t.get("/api/memory-files",e=>{const t=this.server.getConfig().memoryDir;if(!d(t))return e.json([]);try{const r=[],n=c(t,{withFileTypes:!0});for(const e of n){if(!e.isDirectory())continue;const n=v(t,e.name),o=c(n).filter(e=>e.endsWith(".md")).map(e=>{const t=l(v(n,e));return{name:e,size:t.size,modified:t.mtime.toISOString()}}).sort((e,t)=>t.name.localeCompare(e.name));o.length>0&&r.push({sessionKey:e.name,files:o})}return r.sort((e,t)=>e.sessionKey.localeCompare(t.sessionKey)),e.json(r)}catch(t){return H.error(`Failed to list memory files: ${t}`),e.json([])}}),t.get("/api/memory-files/:sessionKey/:fileName",e=>{const t=e.req.param("sessionKey"),r=e.req.param("fileName");if(!/^[\w-]+\.md$/.test(r))return e.json({error:"Invalid file name"},400);if(/[/\\.]/.test(t.replace(/:/g,"")))return e.json({error:"Invalid session key"},400);const n=this.server.getConfig(),o=v(n.memoryDir,t.replace(/:/g,"_"),r);if(!d(o))return e.json({error:"File not found"},404);try{const n=a(o,"utf-8");return e.json({sessionKey:t,fileName:r,content:n})}catch(t){return e.json({error:"Failed to read file"},500)}}),t.get("/api/logs/current",e=>{const t=U();if(!t)return e.json({lines:[],total:0});const r=v(t,"gmab.log");try{const t=a(r,"utf-8").split("\n").filter(e=>e.length>0),n=Math.min(Math.max(parseInt(e.req.query("lines")||"200")||200,1),1e3),o=t.slice(-n);return e.json({lines:o,total:t.length})}catch{return e.json({lines:[],total:0})}}),t.get("/api/logs/current/download",async e=>{const t=U();if(!t)return e.json({error:"Logs not initialized"},503);const r=v(t,"gmab.log"),n="true"===e.req.query("compress");try{if(n){const e=a(r),{gzipSync:t}=await import("node:zlib"),n=t(e);return new Response(n,{headers:{"Content-Type":"application/gzip","Content-Disposition":'attachment; filename="gmab.log.gz"'}})}const e=a(r);return new Response(e,{headers:{"Content-Type":"text/plain","Content-Disposition":'attachment; filename="gmab.log"'}})}catch{return e.json({error:"Log file not found"},404)}}),t.get("/api/logs/files",e=>{const t=U();if(!t)return e.json([]);try{const r=c(t).filter(e=>/^gmab\.\d+\.log$/.test(e)).map(e=>{const r=l(v(t,e));return{name:e,size:r.size,modified:r.mtime.toISOString()}}).sort((e,t)=>e.name.localeCompare(t.name,void 0,{numeric:!0}));return e.json(r)}catch{return e.json([])}}),t.get("/api/logs/files/:name/download",async e=>{const t=U();if(!t)return e.json({error:"Logs not initialized"},503);const r=e.req.param("name");if(!/^gmab\.\d+\.log$/.test(r))return e.json({error:"Invalid file name"},400);const n=v(t,r);try{const e=a(n),{gzipSync:t}=await import("node:zlib"),o=t(e);return new Response(o,{headers:{"Content-Type":"application/gzip","Content-Disposition":`attachment; filename="${r}.gz"`}})}catch{return e.json({error:"File not found"},404)}}),t.get("/api/logs/level",e=>e.json({level:W()})),t.put("/api/logs/level",async e=>{try{const t=(await e.req.json()).level,r=["debug","info","warn","error"];if(!r.includes(t))return e.json({error:"Invalid level. Must be one of: "+r.join(", ")},400);J(t);try{const e=y(process.cwd(),"config.yaml"),{parse:r}=await import("yaml"),n=r(a(e,"utf-8"))||{};n.logLevel=t;const o=s(n);b(e),i(e,o,"utf-8")}catch(e){H.warn(`Failed to persist logLevel to config.yaml: ${e}`)}return H.info(`Log level changed to ${t}`),e.json({ok:!0,level:t})}catch(t){return e.json({error:"Failed to set log level"},400)}}),t.put("/api/logs/verbose",async e=>{try{const t=!!(await e.req.json()).enabled;try{const e=y(process.cwd(),"config.yaml"),{parse:r}=await import("yaml"),n=r(a(e,"utf-8"))||{};n.verboseDebugLogs=t;const o=s(n);b(e),i(e,o,"utf-8")}catch(e){H.warn(`Failed to persist verboseDebugLogs to config.yaml: ${e}`)}return H.info("Verbose debug logs "+(t?"enabled":"disabled")),e.json({ok:!0,enabled:t})}catch(t){return e.json({error:"Failed to set verbose logs"},400)}})}async start(){this.httpServer=t({fetch:this.app.fetch,hostname:this.host,port:this.port}),this.wss=new k({noServer:!0}),this.httpServer.on("upgrade",(e,t,r)=>{const n=new URL(e.url??"/",`http://localhost:${this.port}`),o="/"===this.basePath?"":this.basePath;n.pathname===o+"/ws/nodes"?this.wss.handleUpgrade(e,t,r,t=>{this.wss.emit("connection",t,e)}):t.destroy()}),this.wss.on("connection",e=>{let t=null,r=null,n=!1;const o=()=>{n&&t&&(this.nodeRegistry.unregister(t),n=!1),r&&this.pendingNodes.delete(r);const o=this.server.getWebChatChannel();o&&o.unregisterByWs(e)};e.on("message",o=>{try{const s=JSON.parse(o.toString());if("hello"===s.type){t=s.nodeId;const o=s.signature;if(!t)return void e.close(1008,"Missing nodeId in hello");if(!o)return void e.close(1008,"Missing signature");r=o;const i=this.server.getNodeSignatureDb(),a=i.createOrUpdatePending(t,o,s.hostname??"",s.displayName??"",s.platform??"",s.arch??""),c={displayName:s.displayName,platform:s.platform,arch:s.arch,hostname:s.hostname,capabilities:s.capabilities??[],commands:s.commands??[]};switch(a.status){case"pending":e.send(JSON.stringify({type:"pairing_status",status:"pending"})),this.pendingNodes.set(o,{ws:e,nodeId:t,info:c}),H.info(`Node ${t} (${s.displayName??"unnamed"}) awaiting approval`);break;case"approved":e.send(JSON.stringify({type:"pairing_status",status:"approved"})),this.nodeRegistry.register(t,e,c),n=!0,i.updateLastSeen(a.id),H.info(`Node ${t} (${s.displayName??"unnamed"}) approved and registered`);break;case"revoked":e.send(JSON.stringify({type:"pairing_status",status:"revoked"})),e.close(1008,"Signature revoked")}}else if("command_result"===s.type)this.nodeRegistry.handleCommandResult(s.id,{ok:s.ok,result:s.result,error:s.error});else if("chat"===s.type){if(!n||!t)return;const r=this.server.getWebChatChannel();if(!r)return;const o=this.nodeRegistry.getNode(t),i=S(o?.displayName??"node",t),a=s.chatId,c=a?`${i}/${a}`:i;r.registerConnection(c,e),r.handleNodeChat(c,t,{text:s.text,attachments:s.attachments})}else if("a2ui_action"===s.type){if(!n||!t)return;const e=this.server.getWebChatChannel();if(!e)return;const r=s.userAction;if(!r||!r.name)return;const o=this.nodeRegistry.getNode(t),i=S(o?.displayName??"node",t),a=s.chatId,c=a?`${i}/${a}`:i,l=r.context&&Object.keys(r.context).length>0?` ${JSON.stringify(r.context)}`:"",d=`[A2UI Action] ${r.name} on surface ${r.surfaceId||"main"} (component: ${r.sourceComponentId||"unknown"})${l}`;e.handleNodeChat(c,t,{text:d}),H.info(`A2UI action from node ${t}: ${r.name} on ${r.surfaceId||"main"}`)}else if("dynamic_ui_action"===s.type){if(!n||!t)return;const r=this.server.getWebChatChannel();if(!r)return;const o=s.action;if(!o||!o.activityId)return;const i=s.channel,a=s.chatId;let c;if(i&&a)c=a,r.registerConnection(a,e);else{const e=this.nodeRegistry.getNode(t);c=S(e?.displayName??"node",t)}const l=o.data?` data=${JSON.stringify(o.data)}`:"",d=o.context&&Object.keys(o.context).length>0?` context=${JSON.stringify(o.context)}`:"",u=`[Dynamic UI Action] ${o.type} on #${o.activityId}${l}${d}`;if(i&&"webchat"!==i&&a){const e=this.server.getChannelManager(),n=e?.getAdapter(i);n&&e?(e.setTyping(i,a).catch(()=>{}),this.server.handleMessage({chatId:a,userId:t,channelName:i,text:u,attachments:[]}).then(async t=>{t&&t.trim()&&await e.sendResponse(i,a,t)}).catch(e=>{H.error(`Error handling dynamic_ui_action for ${i}:${a}: ${e}`)})):r.handleNodeChat(c,t,{text:u})}else r.handleNodeChat(c,t,{text:u});H.info(`Dynamic UI action from node ${t}: ${o.type} on #${o.activityId} → ${i??"webchat"}:${c}`)}else"ping"===s.type&&e.send(JSON.stringify({type:"pong"}))}catch{}}),e.on("close",()=>{o()}),e.on("error",e=>{H.error(`Node WS error: ${e.message}`),o()})}),H.info(`Nostromo listening on http://${this.host}:${this.port}${this.basePath}`)}notifyPairingChange(e,t){const r=this.server.getNodeSignatureDb(),n=r.getById(e);if(!n)return;const o=n.signature;if("approved"===t){const t=this.pendingNodes.get(o);t&&(t.ws.send(JSON.stringify({type:"pairing_status",status:"approved"})),this.nodeRegistry.register(t.nodeId,t.ws,t.info),this.pendingNodes.delete(o),r.updateLastSeen(e),H.info(`Node ${t.nodeId} approved via Nostromo`))}else if("revoked"===t){const e=this.pendingNodes.get(o);if(e)return e.ws.send(JSON.stringify({type:"pairing_status",status:"revoked"})),e.ws.close(1008,"Signature revoked"),void this.pendingNodes.delete(o);const t=this.nodeRegistry.listNodes();for(const e of t){const t=this.nodeRegistry.getNode(e.nodeId);if(t&&e.nodeId===n.nodeId){t.ws.send(JSON.stringify({type:"pairing_status",status:"revoked"})),t.ws.close(1008,"Signature revoked"),this.nodeRegistry.unregister(e.nodeId);break}}}}async stop(){this.wss&&this.wss.close(),this.httpServer&&this.httpServer.close(),H.info("Nostromo stopped")}}function V(e){return"string"==typeof e&&/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(e)}const Z={telegram:[{field:"botToken",envSuffix:"BOT_TOKEN"}],discord:[{field:"token",envSuffix:"TOKEN"}],slack:[{field:"botToken",envSuffix:"BOT_TOKEN"},{field:"appToken",envSuffix:"APP_TOKEN"}],msteams:[{field:"appSecret",envSuffix:"APP_SECRET"}],line:[{field:"channelAccessToken",envSuffix:"CHANNEL_ACCESS_TOKEN"},{field:"channelSecret",envSuffix:"CHANNEL_SECRET"}],matrix:[{field:"accessToken",envSuffix:"ACCESS_TOKEN"}]};function G(e){const t=e.match(/^---\r?\n([\s\S]*?)\r?\n---/);if(!t)return{};const r={};for(const e of t[1].split("\n")){const t=e.indexOf(":");if(t<0)continue;const n=e.slice(0,t).trim(),o=e.slice(t+1).trim().replace(/^["']|["']$/g,"");n&&(r[n]=o)}return r}
|
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 j}from"./commands/model.js";import{StopCommand as k}from"./commands/stop.js";import{HelpCommand as D}from"./commands/help.js";import{McpCommand as x}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 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=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),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 k(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getToolServers())),this.commandRegistry.register(new D(()=>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("/")&&this.agentService.isBusy(s))return"I'm busy right now. Please resend this request later.";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});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 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 j}from"./commands/model.js";import{StopCommand as k}from"./commands/stop.js";import{HelpCommand as D}from"./commands/help.js";import{McpCommand as x}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 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=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),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 k(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new x(()=>this.agentService.getToolServers())),this.commandRegistry.register(new D(()=>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("/")&&this.agentService.isBusy(s))return"I'm busy right now. Please resend this request later.";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")}}
|
|
@@ -18,9 +18,9 @@ Your data directory is: {{DATA_DIR}}
|
|
|
18
18
|
|
|
19
19
|
Complete the task above and return the result. Do not engage in conversation beyond the task scope. Be concise and focused.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Time zone
|
|
22
22
|
|
|
23
|
-
{{
|
|
23
|
+
{{TIMEZONE}}
|
|
24
24
|
|
|
25
25
|
## Available tools
|
|
26
26
|
|