@hera-al/server 1.6.2 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/agent/agent-service.d.ts +2 -1
  2. package/dist/agent/agent-service.js +1 -1
  3. package/dist/agent/prompt-builder.js +1 -1
  4. package/dist/agent/session-agent.d.ts +27 -1
  5. package/dist/agent/session-agent.js +1 -1
  6. package/dist/commands/model.d.ts +21 -2
  7. package/dist/commands/model.js +1 -1
  8. package/dist/commands/models.d.ts +4 -2
  9. package/dist/commands/models.js +1 -1
  10. package/dist/commands/status.d.ts +2 -0
  11. package/dist/commands/status.js +1 -1
  12. package/dist/config.d.ts +61 -4
  13. package/dist/config.js +1 -1
  14. package/dist/gateway/channels/telegram.js +1 -1
  15. package/dist/nostromo/nostromo.js +1 -1
  16. package/dist/nostromo/ui-html-layout.js +1 -1
  17. package/dist/nostromo/ui-js-agent.js +1 -1
  18. package/dist/nostromo/ui-js-config.js +1 -1
  19. package/dist/nostromo/ui-js-core.js +1 -1
  20. package/dist/nostromo/ui-styles.js +1 -1
  21. package/dist/pi-agent-provider/index.d.ts +115 -0
  22. package/dist/pi-agent-provider/index.js +1 -0
  23. package/dist/pi-agent-provider/integration-example.d.ts +83 -0
  24. package/dist/pi-agent-provider/integration-example.js +1 -0
  25. package/dist/pi-agent-provider/pi-context-compactor.d.ts +116 -0
  26. package/dist/pi-agent-provider/pi-context-compactor.js +1 -0
  27. package/dist/pi-agent-provider/pi-mcp-bridge.d.ts +38 -0
  28. package/dist/pi-agent-provider/pi-mcp-bridge.js +1 -0
  29. package/dist/pi-agent-provider/pi-message-adapter.d.ts +93 -0
  30. package/dist/pi-agent-provider/pi-message-adapter.js +1 -0
  31. package/dist/pi-agent-provider/pi-query.d.ts +49 -0
  32. package/dist/pi-agent-provider/pi-query.js +1 -0
  33. package/dist/pi-agent-provider/pi-skill-loader.d.ts +15 -0
  34. package/dist/pi-agent-provider/pi-skill-loader.js +1 -0
  35. package/dist/pi-agent-provider/pi-tool-adapter.d.ts +105 -0
  36. package/dist/pi-agent-provider/pi-tool-adapter.js +1 -0
  37. package/dist/pi-agent-provider/pi-tool-executor.d.ts +95 -0
  38. package/dist/pi-agent-provider/pi-tool-executor.js +1 -0
  39. package/dist/pi-agent-provider/pi-types.d.ts +179 -0
  40. package/dist/pi-agent-provider/pi-types.js +1 -0
  41. package/dist/server.js +1 -1
  42. package/dist/stt/stt-loader.js +1 -1
  43. package/dist/tools/pico-tools.d.ts +11 -0
  44. package/dist/tools/pico-tools.js +1 -0
  45. package/package.json +2 -1
@@ -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 S}from"../gateway/channels/webchat.js";import{loadConfig as C,loadRawConfig as $,backupConfig as b}from"../config.js";import{renderSPA as N}from"./ui.js";import{buildSystemPrompt as T}from"../agent/prompt-builder.js";import{loadWorkspaceFiles as q}from"../agent/workspace-files.js";import{resolveBundledDir as I}from"../utils/package-paths.js";import{readKey as P,isDefaultKey as D,validateKey as A,regenerateKey as O,createSession as x,validateSession as E,validateCsrf as R,getCsrfToken as K,destroySession as F,checkRateLimit as L,recordLoginFailure as _,resetLoginAttempts as B}from"./auth.js";import{createLogger as z,getLogsDir as M,getLogLevel as U,setLogLevel as H}from"../utils/logger.js";const W=z("Nostromo"),J="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"?W.debug(`${t.req.method} ${t.req.path} ${t.res.status} ${o}ms`):W.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(N(D(),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(!A(s))return _(r),t.json({error:"Invalid access key"},401);B(r);const{sessionToken:i,csrfToken:a}=x();if(n(t,J,i,{httpOnly:!0,sameSite:"Lax",path:e||"/",maxAge:86400}),D()){const e=O();return W.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,J);return n&&F(n),o(t,J,{path:e||"/"}),t.json({ok:!0})}),t.get("/api/auth/session",e=>{const t=r(e,J);if(!E(t))return e.json({valid:!1},401);const n=K(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,J);if(!E(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(!R(s,e))return t.json({error:"Invalid CSRF token"},403)}return n()}),t.get("/api/config",e=>{const t=$(),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(V)){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&&!Y(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&&!Y(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&&!Y(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&&!Y(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;W.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"),W.info("Config updated via Nostromo"),e.json({ok:!0})}catch(t){return W.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{W.info("Server restart requested via Nostromo");const t=C();return await this.server.reconfigure(t),this.startedAt=Date.now(),this.configHash=this.hashConfigFile(),W.info("Server restarted successfully via Nostromo"),e.json({ok:!0})}catch(t){return W.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 W.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 W.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 W.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 W.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 W.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 W.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"),W.info(`Workspace file saved via Nostromo: ${a}`),e.json({ok:!0})}catch(t){return W.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 W.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}),W.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 W.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 W.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=Z(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 W.info(`Command deleted via Nostromo: ${t}`),e.json({ok:!0})}catch(t){return W.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 W.info(`Command folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return W.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 W.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 W.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 W.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"),W.info(`Command file saved via Nostromo: ${r}`),e.json({ok:!0})}catch(t){return W.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"),W.info(`Standalone command created via Nostromo: ${r}.md`),e.json({ok:!0,file:r+".md"})}catch(t){return W.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=Z(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){W.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),W.info(`Skill deleted via Nostromo: ${t}`),e.json({ok:!0})):e.json({error:"Skill not found"},404)}catch(t){return W.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 W.info(`Skill folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return W.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 W.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 W.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"),W.info(`Skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return W.error(`Skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/skills/bundled",e=>{try{const t=I();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=Z(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 W.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=I();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}),W.info(`Bundled skill installed via Nostromo: ${t}`),e.json({ok:!0,name:t})}catch(t){return W.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=I();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 W.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=I();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"),W.info(`Bundled skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return W.error(`Bundled skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/prompt-simulate",e=>{try{const t=C(),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 W.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=s.models?.find(e=>e.name===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 W.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 W.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=M();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=M();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=M();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=M();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:U()})),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);H(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){W.warn(`Failed to persist logLevel to config.yaml: ${e}`)}return W.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){W.warn(`Failed to persist verboseDebugLogs to config.yaml: ${e}`)}return W.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}),W.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),W.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"ping"===s.type&&e.send(JSON.stringify({type:"pong"}))}catch{}}),e.on("close",()=>{o()}),e.on("error",e=>{W.error(`Node WS error: ${e.message}`),o()})}),W.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),W.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(),W.info("Nostromo stopped")}}function Y(e){return"string"==typeof e&&/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(e)}const V={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 Z(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 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 S}from"../gateway/channels/webchat.js";import{loadConfig as C,loadRawConfig as $,backupConfig as b,resolveModelEntry as N}from"../config.js";import{renderSPA as T}from"./ui.js";import{buildSystemPrompt as q}from"../agent/prompt-builder.js";import{loadWorkspaceFiles as I}from"../agent/workspace-files.js";import{resolveBundledDir as P}from"../utils/package-paths.js";import{readKey as D,isDefaultKey as A,validateKey as O,regenerateKey as x,createSession as E,validateSession as R,validateCsrf as K,getCsrfToken as F,destroySession as L,checkRateLimit as _,recordLoginFailure as B,resetLoginAttempts as z}from"./auth.js";import{createLogger as M,getLogsDir as U,getLogLevel as H,setLogLevel as W}from"../utils/logger.js";const J=M("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(),D(),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"?J.debug(`${t.req.method} ${t.req.path} ${t.res.status} ${o}ms`):J.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(T(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=_(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(!O(s))return B(r),t.json({error:"Invalid access key"},401);z(r);const{sessionToken:i,csrfToken:a}=E();if(n(t,Y,i,{httpOnly:!0,sameSite:"Lax",path:e||"/",maxAge:86400}),A()){const e=x();return J.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&&L(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=F(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=$(),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;J.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"),J.info("Config updated via Nostromo"),e.json({ok:!0})}catch(t){return J.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:D()})),t.post("/api/key/regenerate",e=>{const t=x();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{J.info("Server restart requested via Nostromo");const t=C();return await this.server.reconfigure(t),this.startedAt=Date.now(),this.configHash=this.hashConfigFile(),J.info("Server restarted successfully via Nostromo"),e.json({ok:!0})}catch(t){return J.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 J.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 J.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 J.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 J.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 J.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 J.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"),J.info(`Workspace file saved via Nostromo: ${a}`),e.json({ok:!0})}catch(t){return J.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 J.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}),J.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 J.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 J.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 J.info(`Command deleted via Nostromo: ${t}`),e.json({ok:!0})}catch(t){return J.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 J.info(`Command folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return J.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 J.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 J.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 J.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"),J.info(`Command file saved via Nostromo: ${r}`),e.json({ok:!0})}catch(t){return J.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"),J.info(`Standalone command created via Nostromo: ${r}.md`),e.json({ok:!0,file:r+".md"})}catch(t){return J.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){J.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),J.info(`Skill deleted via Nostromo: ${t}`),e.json({ok:!0})):e.json({error:"Skill not found"},404)}catch(t){return J.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 J.info(`Skill folder uploaded via Nostromo: ${n}`),e.json({ok:!0,name:n})}catch(t){return J.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 J.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 J.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"),J.info(`Skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return J.error(`Skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/skills/bundled",e=>{try{const t=P();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 J.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=P();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}),J.info(`Bundled skill installed via Nostromo: ${t}`),e.json({ok:!0,name:t})}catch(t){return J.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=P();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 J.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=P();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"),J.info(`Bundled skill file saved via Nostromo: ${r}/SKILL.md`),e.json({ok:!0})}catch(t){return J.error(`Bundled skill file save failed: ${t}`),e.json({error:"Save failed"},500)}}),t.get("/api/prompt-simulate",e=>{try{const t=C(),r=I(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=q({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=q({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 J.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 J.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 J.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:H()})),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);W(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){J.warn(`Failed to persist logLevel to config.yaml: ${e}`)}return J.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){J.warn(`Failed to persist verboseDebugLogs to config.yaml: ${e}`)}return J.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}),J.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),J.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"ping"===s.type&&e.send(JSON.stringify({type:"pong"}))}catch{}}),e.on("close",()=>{o()}),e.on("error",e=>{J.error(`Node WS error: ${e.message}`),o()})}),J.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),J.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(),J.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}