@hera-al/server 1.6.12 → 1.6.13

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 (61) hide show
  1. package/bundled/a2ui/SKILL.md +339 -0
  2. package/bundled/buongiorno/SKILL.md +151 -0
  3. package/bundled/council/SKILL.md +168 -0
  4. package/bundled/council/scripts/council.mjs +202 -0
  5. package/bundled/dreaming/SKILL.md +177 -0
  6. package/bundled/google-workspace/SKILL.md +229 -0
  7. package/bundled/google-workspace/scripts/auth.sh +87 -0
  8. package/bundled/google-workspace/scripts/calendar.sh +508 -0
  9. package/bundled/google-workspace/scripts/drive.sh +459 -0
  10. package/bundled/google-workspace/scripts/gmail.sh +452 -0
  11. package/bundled/humanizer/SKILL.md +488 -0
  12. package/bundled/librarian/SKILL.md +155 -0
  13. package/bundled/plasma/SKILL.md +1417 -0
  14. package/bundled/sera/SKILL.md +143 -0
  15. package/bundled/the-skill-guardian/SKILL.md +103 -0
  16. package/bundled/the-skill-guardian/scripts/scan.sh +314 -0
  17. package/bundled/unix-time/SKILL.md +58 -0
  18. package/bundled/wandering/SKILL.md +174 -0
  19. package/bundled/xai-search/SKILL.md +91 -0
  20. package/bundled/xai-search/scripts/search.sh +197 -0
  21. package/dist/a2ui/parser.d.ts +76 -0
  22. package/dist/a2ui/parser.js +1 -0
  23. package/dist/a2ui/types.d.ts +147 -0
  24. package/dist/a2ui/types.js +1 -0
  25. package/dist/a2ui/validator.d.ts +32 -0
  26. package/dist/a2ui/validator.js +1 -0
  27. package/dist/agent/agent-service.d.ts +17 -11
  28. package/dist/agent/agent-service.js +1 -1
  29. package/dist/agent/session-agent.d.ts +1 -1
  30. package/dist/agent/session-agent.js +1 -1
  31. package/dist/agent/session-error-handler.js +1 -1
  32. package/dist/commands/debuga2ui.d.ts +13 -0
  33. package/dist/commands/debuga2ui.js +1 -0
  34. package/dist/commands/debugdynamic.d.ts +13 -0
  35. package/dist/commands/debugdynamic.js +1 -0
  36. package/dist/commands/mcp.d.ts +6 -3
  37. package/dist/commands/mcp.js +1 -1
  38. package/dist/gateway/node-registry.d.ts +29 -1
  39. package/dist/gateway/node-registry.js +1 -1
  40. package/dist/installer/hera.js +1 -1
  41. package/dist/memory/concept-store.d.ts +109 -0
  42. package/dist/memory/concept-store.js +1 -0
  43. package/dist/nostromo/nostromo.js +1 -1
  44. package/dist/server.d.ts +3 -2
  45. package/dist/server.js +1 -1
  46. package/dist/tools/a2ui-tools.d.ts +23 -0
  47. package/dist/tools/a2ui-tools.js +1 -0
  48. package/dist/tools/concept-tools.d.ts +3 -0
  49. package/dist/tools/concept-tools.js +1 -0
  50. package/dist/tools/dynamic-ui-tools.d.ts +25 -0
  51. package/dist/tools/dynamic-ui-tools.js +1 -0
  52. package/dist/tools/node-tools.js +1 -1
  53. package/dist/tools/plasma-client-tools.d.ts +28 -0
  54. package/dist/tools/plasma-client-tools.js +1 -0
  55. package/installationPkg/AGENTS.md +168 -22
  56. package/installationPkg/SOUL.md +56 -0
  57. package/installationPkg/TOOLS.md +126 -0
  58. package/installationPkg/USER.md +54 -1
  59. package/installationPkg/config.example.yaml +145 -34
  60. package/installationPkg/default-jobs.json +77 -0
  61. package/package.json +3 -2
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ConceptStore — SQLite-backed concept graph for navigable knowledge.
3
+ *
4
+ * Tables:
5
+ * concepts — nodes (id, label, access tracking, source)
6
+ * triples — edges (subject, predicate, object)
7
+ * concept_drafts — staging area for live observations, processed by dreaming
8
+ *
9
+ * Write policy:
10
+ * Only dreaming (nightly) and librarian (biweekly) write to concepts/triples.
11
+ * Live conversations only write to concept_drafts (post-it staging area).
12
+ * Migration from CONCEPTS.md populates on first run.
13
+ */
14
+ export interface Concept {
15
+ id: string;
16
+ label: string;
17
+ created_at: string;
18
+ last_accessed: string | null;
19
+ access_count: number;
20
+ source: string;
21
+ }
22
+ export interface Triple {
23
+ id: number;
24
+ subject: string;
25
+ predicate: string;
26
+ object: string;
27
+ created_at: string;
28
+ source: string;
29
+ }
30
+ export interface ConceptDraft {
31
+ id: number;
32
+ text: string;
33
+ context: string | null;
34
+ session_key: string | null;
35
+ created_at: string;
36
+ processed: number;
37
+ }
38
+ export interface QueryResult {
39
+ center: Concept | null;
40
+ concepts: Concept[];
41
+ triples: Triple[];
42
+ }
43
+ export interface PathResult {
44
+ found: boolean;
45
+ path: string[];
46
+ triples: Triple[];
47
+ }
48
+ export interface ConceptStats {
49
+ totalConcepts: number;
50
+ totalTriples: number;
51
+ totalDraftsPending: number;
52
+ orphanConcepts: number;
53
+ neverAccessed: number;
54
+ topAccessed: Array<{
55
+ id: string;
56
+ label: string;
57
+ access_count: number;
58
+ }>;
59
+ }
60
+ export declare class ConceptStore {
61
+ private db;
62
+ constructor(dataDir: string);
63
+ private migrate;
64
+ /**
65
+ * Navigate the graph from a node. Returns the center concept plus
66
+ * all concepts and triples reachable within `depth` hops.
67
+ * Updates access_count on the center node.
68
+ */
69
+ query(nodeId: string, depth?: number): QueryResult;
70
+ /**
71
+ * Find shortest path between two nodes using BFS.
72
+ * Returns the node IDs along the path and the triples connecting them.
73
+ */
74
+ findPath(from: string, to: string, maxDepth?: number): PathResult;
75
+ /**
76
+ * Fuzzy search on concept labels. Returns matching concepts.
77
+ */
78
+ search(query: string, limit?: number): Concept[];
79
+ /**
80
+ * Graph health statistics.
81
+ */
82
+ stats(): ConceptStats;
83
+ /**
84
+ * Add a draft observation to the staging area.
85
+ * Will be processed by dreaming into proper concepts/triples.
86
+ */
87
+ addDraft(text: string, context?: string, sessionKey?: string): number;
88
+ addConcept(id: string, label: string, source?: string): void;
89
+ addTriple(subject: string, predicate: string, object: string, source?: string): boolean;
90
+ removeConcept(id: string): void;
91
+ removeTriple(subject: string, predicate: string, object: string): void;
92
+ updateConceptLabel(id: string, newLabel: string): void;
93
+ getPendingDrafts(): ConceptDraft[];
94
+ markDraftProcessed(id: number): void;
95
+ /**
96
+ * Import concepts and triples from a CONCEPTS.md file (RDF Turtle format).
97
+ * Only runs if the DB is empty and the file exists.
98
+ */
99
+ importFromTurtleIfEmpty(conceptsFilePath: string): void;
100
+ /**
101
+ * Parse RDF Turtle content and insert concepts + triples.
102
+ */
103
+ importTurtle(content: string): {
104
+ concepts: number;
105
+ triples: number;
106
+ };
107
+ close(): void;
108
+ }
109
+ //# sourceMappingURL=concept-store.d.ts.map
@@ -0,0 +1 @@
1
+ import{existsSync as e,readFileSync as t}from"node:fs";import{join as s}from"node:path";import r from"better-sqlite3";import{createLogger as n}from"../utils/logger.js";const c=n("ConceptStore");export class ConceptStore{db;constructor(e){const t=s(e,"concepts.db");this.db=new r(t),this.db.pragma("journal_mode = WAL"),this.db.pragma("foreign_keys = ON"),this.migrate(),c.info(`ConceptStore opened: ${t}`)}migrate(){this.db.exec("\n CREATE TABLE IF NOT EXISTS concepts (\n id TEXT PRIMARY KEY,\n label TEXT NOT NULL,\n created_at TEXT DEFAULT (datetime('now')),\n last_accessed TEXT,\n access_count INTEGER DEFAULT 0,\n source TEXT DEFAULT 'migration'\n );\n\n CREATE TABLE IF NOT EXISTS triples (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n subject TEXT NOT NULL,\n predicate TEXT NOT NULL,\n object TEXT NOT NULL,\n created_at TEXT DEFAULT (datetime('now')),\n source TEXT DEFAULT 'migration'\n );\n\n CREATE TABLE IF NOT EXISTS concept_drafts (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n text TEXT NOT NULL,\n context TEXT,\n session_key TEXT,\n created_at TEXT DEFAULT (datetime('now')),\n processed INTEGER DEFAULT 0\n );\n\n CREATE INDEX IF NOT EXISTS idx_triples_subject ON triples(subject);\n CREATE INDEX IF NOT EXISTS idx_triples_object ON triples(object);\n CREATE INDEX IF NOT EXISTS idx_triples_predicate ON triples(predicate);\n CREATE UNIQUE INDEX IF NOT EXISTS idx_triples_unique ON triples(subject, predicate, object);\n CREATE INDEX IF NOT EXISTS idx_concepts_access ON concepts(access_count, last_accessed);\n CREATE INDEX IF NOT EXISTS idx_drafts_pending ON concept_drafts(processed);\n ")}query(e,t=1){const s=this.db.prepare("SELECT * FROM concepts WHERE id = ?").get(e);if(!s){const s=this.db.prepare("SELECT * FROM concepts WHERE label LIKE ? LIMIT 1").get(`%${e}%`);return s?this.query(s.id,t):{center:null,concepts:[],triples:[]}}this.db.prepare("UPDATE concepts SET last_accessed = datetime('now'), access_count = access_count + 1 WHERE id = ?").run(e);const r=new Set([e]);let n=[e];const c=[];for(let e=0;e<t&&0!==n.length;e++){const e=n.map(()=>"?").join(","),t=this.db.prepare(`SELECT * FROM triples WHERE subject IN (${e})`).all(...n),s=this.db.prepare(`SELECT * FROM triples WHERE object IN (${e})`).all(...n),o=[];for(const e of[...t,...s]){c.push(e);for(const t of[e.subject,e.object])r.has(t)||(r.add(t),o.push(t))}n=o}const o=[...r],E=[];for(let e=0;e<o.length;e+=500){const t=o.slice(e,e+500),s=t.map(()=>"?").join(","),r=this.db.prepare(`SELECT * FROM concepts WHERE id IN (${s})`).all(...t);E.push(...r)}const i=new Set;return{center:s,concepts:E,triples:c.filter(e=>!i.has(e.id)&&(i.add(e.id),!0))}}findPath(e,t,s=6){if(e===t)return{found:!0,path:[e],triples:[]};const r=new Map,n=new Set([e]);let c=[e];for(let o=0;o<s&&0!==c.length;o++){const s=[],o=c.map(()=>"?").join(","),E=this.db.prepare(`SELECT * FROM triples WHERE subject IN (${o})`).all(...c),i=this.db.prepare(`SELECT * FROM triples WHERE object IN (${o})`).all(...c);for(const c of[...E,...i]){const o=[{node:c.object,from:c.subject},{node:c.subject,from:c.object}];for(const{node:E,from:i}of o)if(!n.has(E)&&n.has(i)&&(n.add(E),r.set(E,{parent:i,triple:c}),s.push(E),E===t)){const s=[t],n=[];let c=t;for(;c!==e;){const e=r.get(c);n.unshift(e.triple),s.unshift(e.parent),c=e.parent}return{found:!0,path:s,triples:n}}}c=s}return{found:!1,path:[],triples:[]}}search(e,t=10){return this.db.prepare("SELECT * FROM concepts WHERE label LIKE ? ORDER BY access_count DESC LIMIT ?").all(`%${e}%`,t)}stats(){return{totalConcepts:this.db.prepare("SELECT COUNT(*) as c FROM concepts").get().c,totalTriples:this.db.prepare("SELECT COUNT(*) as c FROM triples").get().c,totalDraftsPending:this.db.prepare("SELECT COUNT(*) as c FROM concept_drafts WHERE processed = 0").get().c,orphanConcepts:this.db.prepare("\n SELECT COUNT(*) as c FROM concepts\n WHERE id NOT IN (SELECT subject FROM triples)\n AND id NOT IN (SELECT object FROM triples)\n ").get().c,neverAccessed:this.db.prepare("SELECT COUNT(*) as c FROM concepts WHERE access_count = 0").get().c,topAccessed:this.db.prepare("SELECT id, label, access_count FROM concepts ORDER BY access_count DESC LIMIT 10").all()}}addDraft(e,t,s){const r=this.db.prepare("INSERT INTO concept_drafts (text, context, session_key) VALUES (?, ?, ?)").run(e,t??null,s??null);return c.info(`Draft added: "${e.slice(0,60)}..." (id=${r.lastInsertRowid})`),r.lastInsertRowid}addConcept(e,t,s="dreaming"){this.db.prepare("INSERT OR IGNORE INTO concepts (id, label, source) VALUES (?, ?, ?)").run(e,t,s)}addTriple(e,t,s,r="dreaming"){try{return this.db.prepare("INSERT OR IGNORE INTO triples (subject, predicate, object, source) VALUES (?, ?, ?, ?)").run(e,t,s,r),!0}catch{return!1}}removeConcept(e){this.db.prepare("DELETE FROM triples WHERE subject = ? OR object = ?").run(e,e),this.db.prepare("DELETE FROM concepts WHERE id = ?").run(e),c.info(`Concept removed: ${e}`)}removeTriple(e,t,s){this.db.prepare("DELETE FROM triples WHERE subject = ? AND predicate = ? AND object = ?").run(e,t,s)}updateConceptLabel(e,t){this.db.prepare("UPDATE concepts SET label = ? WHERE id = ?").run(t,e)}getPendingDrafts(){return this.db.prepare("SELECT * FROM concept_drafts WHERE processed = 0 ORDER BY created_at ASC").all()}markDraftProcessed(e){this.db.prepare("UPDATE concept_drafts SET processed = 1 WHERE id = ?").run(e)}importFromTurtleIfEmpty(s){if(this.db.prepare("SELECT COUNT(*) as c FROM concepts").get().c>0)return;if(!e(s))return void c.info("No CONCEPTS.md found, starting with empty concept graph");c.info(`Importing concepts from ${s}...`);const r=t(s,"utf-8");this.importTurtle(r)}importTurtle(e){let t=0,s=0;const r=this.db.prepare("INSERT OR IGNORE INTO concepts (id, label, source) VALUES (?, ?, 'migration')"),n=this.db.prepare("INSERT OR IGNORE INTO triples (subject, predicate, object, source) VALUES (?, ?, ?, 'migration')");return this.db.transaction(()=>{for(const c of e.split("\n")){const e=c.trim();if(!e||e.startsWith("#")||e.startsWith("@prefix")||e.startsWith("```"))continue;const o=e.match(/^:(\S+)\s+rdfs:label\s+"([^"]+)"\s*\.\s*$/);if(o){const[,e,s]=o;r.run(e,s),t++;continue}const E=e.match(/^:(\S+)\s+rel:(\S+)\s+:(\S+)\s*\.\s*$/);if(E){const[,e,t,c]=E;r.run(e,e),r.run(c,c),n.run(e,t,c),s++;continue}const i=e.match(/^:(\S+)\s+rel:(\S+)\s+"([^"]+)"\s*\.\s*$/);if(i){const[,e,t,c]=i;r.run(e,e),n.run(e,t,c),s++;continue}const p=e.match(/^:(\S+)\s+rel:(\S+)\s+(\S+)\s*\.\s*$/);if(p){const[,e,t,c]=p;"."===c||c.startsWith("rel:")||c.startsWith("rdfs:")||(r.run(e,e),n.run(e,t,c),s++)}}})(),c.info(`Imported ${t} concepts and ${s} triples from Turtle`),{concepts:t,triples:s}}close(){this.db.close(),c.info("ConceptStore closed")}}
@@ -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,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}
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}
package/dist/server.d.ts CHANGED
@@ -21,7 +21,7 @@ export declare class Server {
21
21
  private nodeRegistry;
22
22
  private messageProcessor;
23
23
  private commandRegistry;
24
- private serverToolsServer;
24
+ private serverToolsFactory;
25
25
  private coderSkill;
26
26
  private showToolUse;
27
27
  private subagentsEnabled;
@@ -30,6 +30,7 @@ export declare class Server {
30
30
  private cronService;
31
31
  private browserService;
32
32
  private memorySearch;
33
+ private conceptStore;
33
34
  private whatsappQr;
34
35
  private whatsappConnected;
35
36
  private whatsappError;
@@ -45,7 +46,7 @@ export declare class Server {
45
46
  private executeCronJob;
46
47
  private setupCommands;
47
48
  private registerChannels;
48
- handleMessage(msg: IncomingMessage): Promise<string>;
49
+ handleMessage(msg: IncomingMessage, isRetry?: boolean): Promise<string>;
49
50
  start(): Promise<void>;
50
51
  private initCronAndHeartbeat;
51
52
  getConfig(): AppConfig;
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 i,resolve as o}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 S,loadWorkspaceFiles as y}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 k,DefaultModelCommand as x}from"./commands/model.js";import{StopCommand as j}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as I}from"./commands/mcp.js";import{ModelsCommand as D}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as N}from"./commands/subagents.js";import{CustomSubAgentsCommand as P}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as K}from"./commands/showtool.js";import{UsageCommand as F}from"./commands/usage.js";import{CronService as O}from"./cron/cron-service.js";import{stripHeartbeatToken as B,isHeartbeatContentEffectivelyEmpty as H}from"./cron/heartbeat-token.js";import{createServerToolsServer as L}from"./tools/server-tools.js";import{createCronToolsServer as Q}from"./tools/cron-tools.js";import{createTTSToolsServer as W}from"./tools/tts-tools.js";import{createMemoryToolsServer as z}from"./tools/memory-tools.js";import{createBrowserToolsServer as G}from"./tools/browser-tools.js";import{createPicoToolsServer as q}from"./tools/pico-tools.js";import{BrowserService as V}from"./browser/browser-service.js";import{MemorySearch as J}from"./memory/memory-search.js";import{stripMediaLines as X}from"./utils/media-response.js";import{loadConfig as Y,loadRawConfig as Z,backupConfig as ee,resolveModelEntry as te,modelRefName as se}from"./config.js";import{stringify as ne}from"yaml";import{createLogger as ie}from"./utils/logger.js";import{SessionErrorHandler as oe}from"./agent/session-error-handler.js";import{initStickerCache as re}from"./gateway/channels/telegram/stickers.js";const ae=ie("Server");export class Server{config;tokenDb;sessionDb;nodeSignatureDb;channelManager;agentService;sessionManager;memoryManager=null;nodeRegistry;messageProcessor;commandRegistry;serverToolsServer;coderSkill;showToolUse=!1;subagentsEnabled=!0;customSubAgentsEnabled=!1;chatSettings=new Map;cronService=null;browserService;memorySearch=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),re(e.dataDir),this.commandRegistry=new C,this.setupCommands(),this.channelManager=new h(e,this.tokenDb,e=>this.handleMessage(e)),this.registerChannels(),this.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService();const o=this.createMemorySearch(e);this.browserService=new V;const g=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,l=this.cronService?Q(this.cronService,()=>this.config):void 0,m=e.tts.enabled?W(()=>this.config):void 0,d=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(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;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,l,this.sessionDb,m,o,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,g,d),S(e.dataDir),s(i(e.agent.workspacePath,".claude","skills"),{recursive:!0}),s(i(e.agent.workspacePath,".claude","commands"),{recursive:!0}),s(i(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 O({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=te(e,t.modelRef),n=(s?.useEnvVar?process.env[s.useEnvVar]:s?.apiKey)||"",i=s?.baseURL||"";if(n)return this.memorySearch=new J(e.memoryDir,e.dataDir,{apiKey:n,baseURL:i||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}),z(this.memorySearch);ae.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 i=`${s}:${n}`;e.has(i)||(e.add(i),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),i=e.sessionKey.substring(t+1);"cron"!==n&&i&&(this.channelManager.getAdapter(n)&&s(n,i))}return t}async executeCronJob(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return ae.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=i(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(H(n))return ae.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const o="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=o?"cron":t.channel,a=o?t.name:t.chatId;ae.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=B(h,e);if(s)return ae.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();ae.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g))),await Promise.allSettled(e.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g),await this.channelManager.releaseTyping(t.channel,t.chatId).catch(()=>{});return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new k(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,i=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),o=this.sessionManager.getModel(e)||this.config.agent.model,r=te(this.config,o),a=i(r?.name??se(o)),c=i(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 x(()=>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 i=this.config.agent.picoAgent;if(i?.enabled&&Array.isArray(i.modelRefs)){const t=s?.name??e,n=i.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=i.modelRefs.splice(n,1);i.modelRefs.unshift(e)}}try{const e=o(process.cwd(),"config.yaml"),s=Z(e);s.agent||(s.agent={}),s.agent.model=n,i?.enabled&&Array.isArray(i.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...i.modelRefs]),ee(e),t(e,ne(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new D(()=>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 K(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new P(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=te(this.config,t),i=s?te(this.config,s):void 0;return{agentModel:n?.id??t,agentModelName:n?.name??se(t),fallbackModel:i?.id??s,fallbackModelName:i?.name??(s?se(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 j(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new I(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new F(e=>this.agentService.getUsage(e)))}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){ae.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){const t=`${e.channelName}:${e.chatId}`,s=e.text?e.text.length>15?e.text.slice(0,15)+"...":e.text:"[no text]";ae.info(`Message from ${t} (user=${e.userId}, ${e.username??"?"}): ${s}`),this.config.verboseDebugLogs&&ae.debug(`Message from ${t} full text: ${e.text??"[no text]"}`);try{if(e.text){if(e.text.startsWith("__ask:")){const s=e.text.substring(6);return this.agentService.resolveQuestion(t,s),""}if(this.agentService.hasPendingQuestion(t)){const s=e.text.trim();return this.agentService.resolveQuestion(t,s),`Selected: ${s}`}if(e.text.startsWith("__tool_perm:")){const s="__tool_perm:approve"===e.text;return this.agentService.resolvePermission(t,s),s?"Tool approved.":"Tool denied."}if(this.agentService.hasPendingPermission(t)){const s=e.text.trim().toLowerCase();if("approve"===s||"approva"===s)return this.agentService.resolvePermission(t,!0),"Tool approved.";if("deny"===s||"vieta"===s||"blocca"===s)return this.agentService.resolvePermission(t,!1),"Tool denied."}}const s=!0===e.__passthrough;if(!s&&e.text&&this.commandRegistry.isCommand(e.text)){const s=await this.commandRegistry.dispatch(e.text,{sessionKey:t,chatId:e.chatId,channelName:e.channelName,userId:e.userId});if(s)return s.passthrough?this.handleMessage({...e,text:s.passthrough,__passthrough:!0}):(s.resetSession?(this.agentService.destroySession(t),this.sessionManager.resetSession(t),this.memoryManager&&this.memoryManager.clearSession(t)):s.resetAgent&&this.agentService.destroySession(t),s.buttons&&s.buttons.length>0?(await this.channelManager.sendButtons(e.channelName,e.chatId,s.text,s.buttons),""):s.text)}if(!s&&e.text?.startsWith("/")&&this.agentService.isBusy(t))return"I'm busy right now. Please resend this request later.";const n=this.sessionManager.getOrCreate(t),i=await this.messageProcessor.process(e),o=u(i,void 0,{sessionKey:t,channel:e.channelName,chatId:e.chatId});ae.debug(`[${t}] Prompt to agent (${o.text.length} chars): ${this.config.verboseDebugLogs?o.text:o.text.slice(0,15)+"..."}${o.images.length>0?` [+${o.images.length} image(s)]`:""}`);const r=n.model,a={sessionKey:t,channel:e.channelName,chatId:e.chatId,sessionId:n.sessionId??"",memoryFile:this.memoryManager?this.memoryManager.getConversationFile(t):"",attachmentsDir:this.memoryManager?this.memoryManager.getAttachmentsDir(t):""},c=y(this.config.dataDir),h={config:this.config,sessionContext:a,workspaceFiles:c,mode:"full",hasNodeTools:this.agentService.hasNodeTools(),hasMessageTools:this.agentService.hasMessageTools(),coderSkill:this.getChatSetting(t,"coderSkill")??this.coderSkill,toolServers:this.agentService.getToolServers()},g=b(h),l=b({...h,mode:"minimal"});ae.debug(`[${t}] System prompt (${g.length} chars): ${this.config.verboseDebugLogs?g:g.slice(0,15)+"..."}`);try{const s=await this.agentService.sendMessage(t,o,n.sessionId,g,l,r,this.getChatSetting(t,"coderSkill")??this.coderSkill,this.getChatSetting(t,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(t,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(t,"sandboxEnabled")??!1);if(s.sessionReset){if("[AGENT_CLOSED]"===s.response)return ae.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),"";{const e={sessionKey:t,sessionId:n.sessionId,error:new Error("Session corruption detected"),timestamp:new Date},s=oe.analyzeError(e.error,e),i=oe.getRecoveryStrategy(s);return ae.warn(`[${t}] ${i.message}`),this.sessionManager.updateSessionId(t,""),i.clearSession&&(this.agentService.destroySession(t),this.memoryManager&&this.memoryManager.clearSession(t)),"[SESSION_CORRUPTED] The previous session is no longer valid. Use /new to start a new one."}}if(ae.debug(`[${t}] Response from agent (session=${s.sessionId}, len=${s.response.length}): ${this.config.verboseDebugLogs?s.response:s.response.slice(0,15)+"..."}`),s.sessionId&&this.sessionManager.updateSessionId(t,s.sessionId),this.memoryManager&&"cron"!==e.userId){const e=(o.text||"[media]").trim();await this.memoryManager.append(t,"user",e,i.savedFiles.length>0?i.savedFiles:void 0),await this.memoryManager.append(t,"assistant",X(s.fullResponse??s.response))}if("max_turns"===s.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 s.response?s.response+e:e.trim()}if("max_budget"===s.errorType){const e="\n\n[MAX_BUDGET] The agent reached the maximum budget for this request.";return s.response?s.response+e:e.trim()}if("refusal"===s.stopReason){const e="\n\n[REFUSAL] The model declined to fulfill this request.";return s.response?s.response+e:e.trim()}if("max_tokens"===s.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 s.response?s.response+e:e.trim()}return s.response}catch(e){const s=e instanceof Error?e.message:String(e);return s.includes("SessionAgent closed")||s.includes("agent closed")?(ae.info(`[${t}] Agent closed during restart, keeping session ID for resume on next message`),""):(ae.error(`Agent error for ${t}: ${e}`),`Error: ${s}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){ae.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"),ae.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{ae.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),ae.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}),ae.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}),ae.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?ae.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?ae.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):ae.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(){ae.info("Trigger restart requested");const e=Y();await this.reconfigure(e),this.notifyAllChannels("Gateway restarted. Agent is alive!").catch(()=>{})}async notifyAllChannels(e){const t=this.collectBroadcastTargets();ae.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),ae.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){ae.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&&(ae.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>ae.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){ae.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),i=s.substring(n+1),o=this.channelManager.getAdapter(t);if(o)try{await o.sendText(i,`Session renewed automatically after ${e}h of inactivity. Starting fresh!`)}catch(e){ae.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}ae.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){ae.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.serverToolsServer=L(()=>this.triggerRestart(),e.timezone),this.cronService=this.createCronService(),this.stopMemorySearch();const n=this.createMemorySearch(e);await this.browserService.reconfigure(e.browser);const i=e.browser?.enabled?G({nodeRegistry:this.nodeRegistry,config:e}):void 0,o=this.cronService?Q(this.cronService,()=>this.config):void 0,r=e.tts.enabled?W(()=>this.config):void 0,a=e.agent.picoAgent?.enabled&&Array.isArray(e.agent.picoAgent.modelRefs)&&e.agent.picoAgent.modelRefs.length>0?q({getConfig:()=>this.config,getSubagentSystemPrompt:()=>{const e=y(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;this.agentService=new f(e,this.nodeRegistry,this.channelManager,this.serverToolsServer,o,this.sessionDb,r,n,e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,i,a),S(e.dataDir),await this.channelManager.startAll(),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.startAutoRenewTimer(),ae.info("Server reconfigured successfully")}async stop(){ae.info("Shutting down..."),this.stopAutoRenewTimer(),this.cronService&&this.cronService.stop(),this.memorySearch&&this.memorySearch.stop(),await this.browserService.stop(),this.agentService.destroyAll(),await this.channelManager.stopAll(),this.sessionManager.destroy(),this.sessionDb.close(),this.tokenDb.close(),this.nodeSignatureDb.close(),ae.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 m}from"./gateway/channels/whatsapp.js";import{WebChatChannel as l}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 j,DefaultModelCommand as k}from"./commands/model.js";import{StopCommand as x}from"./commands/stop.js";import{HelpCommand as $}from"./commands/help.js";import{McpCommand as D}from"./commands/mcp.js";import{ModelsCommand as I}from"./commands/models.js";import{CoderCommand as E}from"./commands/coder.js";import{SandboxCommand as _}from"./commands/sandbox.js";import{SubAgentsCommand as P}from"./commands/subagents.js";import{CustomSubAgentsCommand as N}from"./commands/customsubagents.js";import{StatusCommand as U}from"./commands/status.js";import{ShowToolCommand as F}from"./commands/showtool.js";import{UsageCommand as K}from"./commands/usage.js";import{DebugA2UICommand as O}from"./commands/debuga2ui.js";import{DebugDynamicCommand as B}from"./commands/debugdynamic.js";import{CronService as H}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 me}from"./gateway/channels/telegram/stickers.js";const le=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),me(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"),m=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,m?()=>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 H({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}),q(this.memorySearch);le.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(t){const s=this.config.cron.broadcastEvents;if(!s&&!this.channelManager.getAdapter(t.channel))return le.warn(`Cron job "${t.name}": skipped (channel "${t.channel}" is not active)`),{response:"",delivered:!1};if(t.suppressToken&&"__heartbeat"===t.name){const s=o(this.config.dataDir,"HEARTBEAT.md");if(n(s))try{const n=e(s,"utf-8");if(Q(n))return le.info(`Cron job "${t.name}": skipped (HEARTBEAT.md is empty)`),{response:"",delivered:!1}}catch{}}const i="boolean"==typeof t.isolated?t.isolated:this.config.cron.isolated,r=i?"cron":t.channel,a=i?t.name:t.chatId;le.info(`Cron job "${t.name}": session=${r}:${a}, delivery=${t.channel}:${t.chatId}${s?" (broadcast)":""}`);const c={chatId:a,userId:"cron",channelName:r,text:t.message,attachments:[]},h=await this.handleMessage(c);let g=h;if(t.suppressToken){const e=this.config.cron.heartbeat.ackMaxChars,{shouldSkip:s,text:n}=L(h,e);if(s)return le.info(`Cron job "${t.name}": response suppressed (HEARTBEAT_OK)`),{response:h,delivered:!1};g=n}if(s){const e=this.collectBroadcastTargets();le.info(`Cron job "${t.name}": broadcasting to ${e.length} target(s)`),await Promise.allSettled(e.map(e=>this.channelManager.sendResponse(e.channel,e.chatId,g))),await Promise.allSettled(e.map(e=>this.channelManager.releaseTyping(e.channel,e.chatId)))}else await this.channelManager.sendResponse(t.channel,t.chatId,g),await this.channelManager.releaseTyping(t.channel,t.chatId).catch(()=>{});return{response:g,delivered:!0}}setupCommands(){this.commandRegistry.register(new A),this.commandRegistry.register(new T);const e=()=>this.config.agent.picoAgent??{enabled:!1,modelRefs:[]};this.commandRegistry.register(new j(()=>this.config.models??[],async(e,t)=>{const s=this.config.models?.find(e=>e.id===t),n=this.config.agent.picoAgent,o=e=>!(!n?.enabled||!Array.isArray(n.modelRefs))&&n.modelRefs.some(t=>t.split(":")[0]===e),i=this.sessionManager.getModel(e)||this.config.agent.model,r=re(this.config,i),a=o(r?.name??ae(i)),c=o(s?.name??t);this.sessionManager.setModel(e,t),this.agentService.destroySession(e);const h=a||c;return h&&this.sessionManager.resetSession(e),h},e)),this.commandRegistry.register(new k(()=>this.config.models??[],async e=>{const s=this.config.models?.find(t=>t.id===e),n=s?`${s.name}:${s.id}`:e;this.config.agent.model=n;const o=this.config.agent.picoAgent;if(o?.enabled&&Array.isArray(o.modelRefs)){const t=s?.name??e,n=o.modelRefs.findIndex(e=>e.split(":")[0]===t);if(n>0){const[e]=o.modelRefs.splice(n,1);o.modelRefs.unshift(e)}}try{const e=i(process.cwd(),"config.yaml"),s=oe(e);s.agent||(s.agent={}),s.agent.model=n,o?.enabled&&Array.isArray(o.modelRefs)&&(s.agent.picoAgent||(s.agent.picoAgent={}),s.agent.picoAgent.modelRefs=[...o.modelRefs]),ie(e),t(e,ce(s),"utf-8")}catch{}},e)),this.commandRegistry.register(new I(()=>this.config.models??[],e)),this.commandRegistry.register(new E(e=>this.getChatSetting(e,"coderSkill")??this.coderSkill,(e,t)=>this.setChatSetting(e,"coderSkill",t))),this.commandRegistry.register(new _(e=>this.getChatSetting(e,"sandboxEnabled")??!1,(e,t)=>this.setChatSetting(e,"sandboxEnabled",t))),this.commandRegistry.register(new F(e=>this.getChatSetting(e,"showToolUse")??this.showToolUse,(e,t)=>this.setChatSetting(e,"showToolUse",t))),this.commandRegistry.register(new P(e=>this.getChatSetting(e,"subagentsEnabled")??this.subagentsEnabled,(e,t)=>this.setChatSetting(e,"subagentsEnabled",t))),this.commandRegistry.register(new N(e=>this.getChatSetting(e,"customSubAgentsEnabled")??this.customSubAgentsEnabled,(e,t)=>this.setChatSetting(e,"customSubAgentsEnabled",t),()=>this.config)),this.commandRegistry.register(new U(e=>{const t=this.sessionManager.getModel(e)||this.config.agent.model,s=this.config.agent.mainFallback,n=re(this.config,t),o=s?re(this.config,s):void 0;return{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 x(e=>this.agentService.interrupt(e))),this.commandRegistry.register(new D(()=>this.agentService.getToolServers())),this.commandRegistry.register(new $(()=>this.agentService.getSdkSlashCommands())),this.commandRegistry.register(new K(e=>this.agentService.getUsage(e))),this.commandRegistry.register(new O(this.nodeRegistry)),this.commandRegistry.register(new B(this.nodeRegistry))}registerChannels(){if(this.config.channels.telegram.enabled){const e=this.config.channels.telegram.accounts;for(const[t,s]of Object.entries(e)){if(!s.botToken){le.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 m(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 l),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]";le.info(`Message from ${s} (user=${e.userId}, ${e.username??"?"}): ${n}`),this.config.verboseDebugLogs&&le.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});le.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()},m=b(g),l=b({...g,mode:"minimal"});le.debug(`[${s}] System prompt (${m.length} chars): ${this.config.verboseDebugLogs?m:m.slice(0,15)+"..."}`);try{const n=await this.agentService.sendMessage(s,r,o.sessionId,m,l,a,this.getChatSetting(s,"coderSkill")??this.coderSkill,this.getChatSetting(s,"subagentsEnabled")??this.subagentsEnabled,this.getChatSetting(s,"customSubAgentsEnabled")??this.customSubAgentsEnabled,this.getChatSetting(s,"sandboxEnabled")??!1);if(n.sessionReset){if("[AGENT_CLOSED]"===n.response)return le.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 le.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.":(le.info(`[${s}] Retrying with fresh session after: ${i}`),this.handleMessage(e,!0))}}if(le.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")?(le.info(`[${s}] Agent closed during restart, keeping session ID for resume on next message`),""):(le.error(`Agent error for ${s}: ${e}`),`Error: ${t}`)}}finally{await this.channelManager.releaseTyping(e.channelName,e.chatId).catch(()=>{})}}async start(){le.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"),le.info(`Enabled channels: ${e.join(", ")||"none"}`),await this.channelManager.startAll(),await this.browserService.start(this.config.browser).catch(e=>{le.warn(`Browser service failed to start: ${e}`)}),this.memorySearch&&await this.memorySearch.start(),this.cronService&&await this.initCronAndHeartbeat(),this.nodeRegistry.startPingLoop(),this.startAutoRenewTimer(),le.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}),le.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}),le.info("Heartbeat job auto-added")}else t&&t.enabled&&(await this.cronService.update(t.id,{enabled:!1}),e.enabled&&!n?le.warn("Heartbeat job disabled: message is empty or too short (minimum 15 characters)"):e.enabled&&!s?le.warn(`Heartbeat job disabled: channel "${e.channel}" is not active`):le.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(){le.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();le.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),le.debug(`Notified ${t.channel}:${t.chatId}`)}catch(e){le.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&&(le.info(`AutoRenew enabled — resetting sessions inactive for ${e}h (checking every 15 min)`),this.autoRenewTimer=setInterval(()=>{this.autoRenewStaleSessions().catch(e=>le.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){le.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){le.warn(`AutoRenew: failed to send courtesy message to ${s}: ${e}`)}}le.info(`AutoRenew: session reset — ${s}`)}}}async reconfigure(e){le.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(),le.info("Server reconfigured successfully")}async stop(){le.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(),le.info("Server stopped")}}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A2UI Tool Server
3
+ *
4
+ * Provides MCP tools for the agent to generate and manage A2UI surfaces
5
+ * on connected nodes with A2UI capability.
6
+ *
7
+ * Tools:
8
+ * - a2ui_validate: Validate JSONL against official A2UI v0.8 schema (ALWAYS USE FIRST!)
9
+ * - a2ui_list_nodes: List all A2UI-capable nodes
10
+ * - a2ui_render: Render A2UI surface on specific node
11
+ * - a2ui_update: Update existing surface (streaming support)
12
+ * - a2ui_reset: Clear surface from node
13
+ * - a2ui_text: Quick text surface helper
14
+ */
15
+ import type { NodeRegistry } from "../gateway/node-registry.js";
16
+ export interface A2UIToolsDeps {
17
+ nodeRegistry: NodeRegistry;
18
+ }
19
+ /**
20
+ * Create A2UI tools MCP server.
21
+ */
22
+ export declare function createA2UIToolsServer(deps: A2UIToolsDeps): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
23
+ //# sourceMappingURL=a2ui-tools.d.ts.map
@@ -0,0 +1 @@
1
+ import{createSdkMcpServer as e,tool as t}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{parseA2UIJsonl as a,buildTextSurfaceJsonl as o,validateA2UIMessages as r}from"../a2ui/types.js";import{validateA2UIJsonl as i,formatValidationErrors as s}from"../a2ui/validator.js";import{createLogger as d}from"../utils/logger.js";const l=d("A2UITools");async function c(e,t,n,a){const o=e.nodeRegistry.getNode(t);if(!o)throw new Error(`Node ${t} not found or disconnected`);if(!o.capabilities?.includes("a2ui"))throw new Error(`Node ${t} (${o.displayName??"unnamed"}) does not support A2UI capability`);const i=r(n);i.error&&l.warn(`A2UI validation warning for node ${t}: ${i.error}`);const s={type:"a2ui_surface",messages:n,jsonl:a.trim()};try{return o.ws.send(JSON.stringify(s)),l.info(`A2UI surface sent to node ${t} (${o.displayName??"unnamed"}): ${n.length} messages, ${i.surfaceIds.size} surface(s)`),{nodeId:o.nodeId,displayName:o.displayName??"unnamed"}}catch(e){const n=e instanceof Error?e.message:String(e);throw l.error(`Failed to send A2UI to node ${t}: ${n}`),new Error(`Failed to send A2UI to node ${t}: ${n}`)}}function u(e,t){const n=e.nodeRegistry.findNodesWithCapability("a2ui");if(0===n.length)throw new Error("No A2UI-capable nodes connected. Ask user to connect ElectroNode or OSXNode.");if(t){if(!n.find(e=>e.nodeId===t)){const e=n.map(e=>`${e.displayName??"unnamed"} (${e.nodeId})`).join(", ");throw new Error(`Node ${t} not found or doesn't support A2UI. Available: ${e}`)}return t}return n.length>1&&l.warn(`Multiple A2UI nodes available (${n.length}), using first: ${n[0].displayName}`),n[0].nodeId}export function createA2UIToolsServer(d){return e({name:"a2ui-tools",version:"1.0.0",tools:[t("a2ui_validate","Validate A2UI JSONL against official Google v0.8 JSON Schema.\n\n**CRITICAL: ALWAYS call this tool BEFORE a2ui_render or a2ui_update.**\n\nThis implements Google's recommended validation-first approach:\n1. Generate A2UI JSONL\n2. Call a2ui_validate to check for errors\n3. If validation fails, fix errors and retry validation\n4. Only when validation passes, call a2ui_render\n\nReturns detailed error messages if validation fails, allowing you to fix and retry.\n\n**Example workflow:**\n1. Generate JSONL for a booking form\n2. Call a2ui_validate(jsonl) → validation fails with \"Button: missing required property 'child'\"\n3. Fix: add child Text component to Button\n4. Call a2ui_validate(jsonl) → validation passes ✅\n5. Call a2ui_render(nodeId, jsonl) → surface rendered successfully",{jsonl:n.string().describe("A2UI v0.8 JSONL string to validate (one JSON message per line)")},async e=>{try{const t=i(e.jsonl);return{content:[{type:"text",text:s(t)}],isError:!t.valid}}catch(e){const t=e instanceof Error?e.message:String(e);return l.error(`A2UI validation tool error: ${t}`),{content:[{type:"text",text:`Validation error: ${t}`}],isError:!0}}}),t("a2ui_list_nodes","List all connected nodes that support A2UI rendering capability",{},async()=>{const e=d.nodeRegistry.findNodesWithCapability("a2ui");if(0===e.length)return{content:[{type:"text",text:"No A2UI-capable nodes connected. User needs to connect ElectroNode or OSXNode."}]};const t=e.map(e=>({nodeId:e.nodeId,displayName:e.displayName??"unnamed",platform:e.platform??"unknown",hostname:e.hostname??""}));return{content:[{type:"text",text:`A2UI-capable nodes (${t.length}):\n${t.map(e=>`• ${e.displayName} — ${e.nodeId} (${e.platform}${e.hostname?`, ${e.hostname}`:""})`).join("\n")}`}]}}),t("a2ui_render",'Render an A2UI surface on a connected node (ElectroNode, OSXNode, etc).\n\n**AUTOMATIC VALIDATION:** This tool automatically validates your JSONL against the official Google A2UI v0.8 JSON Schema before rendering. If validation fails, you\'ll receive detailed error messages to fix and retry.\n\nThe surface is defined as A2UI v0.8 JSONL — one JSON message per line.\n\n**Message types:**\n- surfaceUpdate: define/update components on a surface\n- beginRendering: tell the renderer to display the surface (required!)\n- dataModelUpdate: update data model values\n- deleteSurface: remove a surface\n\n**Component types (PascalCase):**\nLayout: Column, Row, Card, Divider, Tabs, Modal, List\nContent: Text, Image, Icon, AudioPlayer, Video\nInput: Button, TextField, CheckBox, MultipleChoice, Slider, DateTimeInput\n\n**CRITICAL SYNTAX RULES:**\n1. Button: MUST use "child" (Text component ID), NOT "label". Action MUST be {"event":{"name":"...","context":[...]}}\n2. TextField: MUST use "text":{"path":"/fieldname"}, NOT "dataRef"\n3. All data paths: MUST start with "/" (e.g., "/name" not "name")\n\n**Example JSONL for a booking form:**\n{"surfaceUpdate":{"surfaceId":"booking","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","guests","date","submit"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Book a Table"},"usageHint":"h2"}}},{"id":"guests","component":{"TextField":{"label":{"literalString":"Number of Guests"},"text":{"path":"/guests"}}}},{"id":"date","component":{"DateTimeInput":{"label":{"literalString":"Date"},"value":{"path":"/date"}}}},{"id":"submit","component":{"Button":{"child":"submit_text","action":{"event":{"name":"submit_booking","context":[{"key":"guests","value":{"path":"/guests"}},{"key":"date","value":{"path":"/date"}}]}}}}},{"id":"submit_text","component":{"Text":{"text":{"literalString":"Submit Booking"}}}}]}}\n{"dataModelUpdate":{"surfaceId":"booking","path":"/","value":{"guests":"2","date":"2026-02-20"}}}\n{"beginRendering":{"surfaceId":"booking","root":"root"}}\n\n**User actions:** When user interacts (clicks button, submits form), you\'ll receive a message like:\n[A2UI Action] submit_booking on surface booking (component: submit) {"guests":"2","date":"2026-02-20"}\n\nYou can then respond with text or update the surface.',{nodeId:n.string().optional().describe("Target node ID. Omit to use first available. Use a2ui_list_nodes to see options."),jsonl:n.string().describe("A2UI v0.8 JSONL string — one JSON message object per line. Must include surfaceUpdate + beginRendering.")},async e=>{try{const t=i(e.jsonl);if(!t.valid){const e=s(t);return l.warn(`A2UI render validation failed: ${t.errors?.length} error(s)`),{content:[{type:"text",text:`❌ A2UI Validation Failed\n\n${e}\n\nPlease fix these errors and try again.`}],isError:!0}}const n=a(e.jsonl);l.info(`A2UI render: validation passed, ${n.length} messages`);const o=r(n);if(!o.hasRendering)return{content:[{type:"text",text:"Error: No beginRendering message found. The surface will not be visible. Add a beginRendering message."}],isError:!0};const p=u(d,e.nodeId),f=await c(d,p,n,e.jsonl),m=Array.from(o.surfaceIds).join(", ");return{content:[{type:"text",text:`✅ A2UI surface rendered on ${f.displayName} (${f.nodeId}).\nSurfaces: ${m}\nMessages: ${n.length}`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return l.error(`A2UI render failed: ${t}`),{content:[{type:"text",text:`A2UI render error: ${t}`}],isError:!0}}}),t("a2ui_update","Update an existing A2UI surface with new components or data.\n\n**AUTOMATIC VALIDATION:** This tool automatically validates your JSONL against the official Google A2UI v0.8 JSON Schema before updating. If validation fails, you'll receive detailed error messages to fix and retry.\n\nUse this to:\n- Add/update components on existing surface (surfaceUpdate)\n- Update data model (dataModelUpdate)\n- Change surface state progressively\n\nUnlike a2ui_render, you don't need beginRendering (surface is already visible).",{nodeId:n.string().optional().describe("Target node ID (omit to use first available)"),jsonl:n.string().describe("A2UI JSONL with surfaceUpdate and/or dataModelUpdate messages (no beginRendering needed)")},async e=>{try{const t=i(e.jsonl);if(!t.valid){const e=s(t);return l.warn(`A2UI update validation failed: ${t.errors?.length} error(s)`),{content:[{type:"text",text:`❌ A2UI Validation Failed\n\n${e}\n\nPlease fix these errors and try again.`}],isError:!0}}const n=a(e.jsonl);l.info(`A2UI update: validation passed, ${n.length} messages`);const o=u(d,e.nodeId),r=await c(d,o,n,e.jsonl);return{content:[{type:"text",text:`✅ A2UI surface updated on ${r.displayName} (${r.nodeId}). Messages: ${n.length}`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return l.error(`A2UI update failed: ${t}`),{content:[{type:"text",text:`A2UI update error: ${t}`}],isError:!0}}}),t("a2ui_reset","Remove/clear an A2UI surface from a node. Use this to close forms, clear dashboards, etc.",{nodeId:n.string().optional().describe("Target node ID (omit to use first available)"),surfaceId:n.string().optional().describe("Surface ID to delete (default: 'main'). Use 'all' to clear all surfaces.")},async e=>{try{const t=e.surfaceId||"main",n=u(d,e.nodeId);if("all"===t){const e=["main","booking","dashboard","form"].map(e=>({deleteSurface:{surfaceId:e}})),t=e.map(e=>JSON.stringify(e)).join("\n"),a=await c(d,n,e,t);return{content:[{type:"text",text:`All A2UI surfaces cleared on ${a.displayName} (${a.nodeId})`}]}}const o=JSON.stringify({deleteSurface:{surfaceId:t}}),r=a(o),i=await c(d,n,r,o);return{content:[{type:"text",text:`A2UI surface "${t}" cleared on ${i.displayName} (${i.nodeId})`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return l.error(`A2UI reset failed: ${t}`),{content:[{type:"text",text:`A2UI reset error: ${t}`}],isError:!0}}}),t("a2ui_text","Quick helper: render a simple text surface. For more complex UIs, use a2ui_render with full JSONL.",{nodeId:n.string().optional().describe("Target node ID (omit to use first available)"),text:n.string().describe("Text content to display"),surfaceId:n.string().optional().describe("Surface ID (default: 'main')")},async e=>{try{const t=e.surfaceId||"main",n=o(e.text,t),r=a(n),i=u(d,e.nodeId),s=await c(d,i,r,n);return{content:[{type:"text",text:`Text surface displayed on ${s.displayName} (${s.nodeId})`}]}}catch(e){const t=e instanceof Error?e.message:String(e);return l.error(`A2UI text failed: ${t}`),{content:[{type:"text",text:`A2UI text error: ${t}`}],isError:!0}}})]})}
@@ -0,0 +1,3 @@
1
+ import type { ConceptStore } from "../memory/concept-store.js";
2
+ export declare function createConceptToolsServer(store: ConceptStore): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
3
+ //# sourceMappingURL=concept-tools.d.ts.map
@@ -0,0 +1 @@
1
+ import{createSdkMcpServer as t,tool as e}from"@anthropic-ai/claude-agent-sdk";import{z as n}from"zod";import{createLogger as o}from"../utils/logger.js";const r=o("ConceptTools");export function createConceptToolsServer(o){return t({name:"concept-tools",version:"1.0.0",tools:[e("concept_query","Navigate the concept graph from a node. Returns the center concept and all connected concepts/triples within the specified depth. Use this to explore relationships between people, projects, tools, and ideas.",{node:n.string().describe("Concept ID or label fragment to start from (e.g. 'lorenzo', 'dexter', 'savona')"),depth:n.number().optional().describe("How many hops to traverse (default 1, max 3)")},async t=>{try{const e=Math.min(t.depth??1,3),n=o.query(t.node,e);if(!n.center){const e=o.search(t.node,3);return 0===e.length?{content:[{type:"text",text:`No concept found matching "${t.node}". Try concept_search for fuzzy matching.`}]}:{content:[{type:"text",text:`No exact match for "${t.node}". Did you mean:\n${e.map(t=>`- ${t.id} (${t.label})`).join("\n")}`}]}}const r=[];if(r.push(`## ${n.center.label} (${n.center.id})`),r.push(`Accessed ${n.center.access_count} times | Source: ${n.center.source}`),r.push(""),n.triples.length>0){r.push("### Relations");for(const t of n.triples){const e=n.concepts.find(e=>e.id===t.subject)?.label??t.subject,o=n.concepts.find(e=>e.id===t.object)?.label??t.object;r.push(`- ${e} → ${t.predicate} → ${o}`)}}if(n.concepts.length>1){r.push(""),r.push(`### Connected concepts (${n.concepts.length-1})`);for(const t of n.concepts)t.id!==n.center.id&&r.push(`- ${t.id}: ${t.label}`)}return{content:[{type:"text",text:r.join("\n")}]}}catch(t){const e=t instanceof Error?t.message:String(t);return r.error(`concept_query error: ${e}`),{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}}),e("concept_path","Find the shortest path between two concepts in the graph. Useful for discovering indirect connections (e.g. how 'dexter' relates to 'savona').",{from:n.string().describe("Starting concept ID"),to:n.string().describe("Target concept ID")},async t=>{try{const e=o.findPath(t.from,t.to);if(!e.found)return{content:[{type:"text",text:`No path found between "${t.from}" and "${t.to}" within 6 hops.`}]};const n=[];if(n.push(`### Path: ${t.from} → ${t.to} (${e.path.length-1} hops)`),n.push(""),n.push(e.path.join(" → ")),n.push(""),e.triples.length>0){n.push("### Connections");for(const t of e.triples)n.push(`- ${t.subject} → ${t.predicate} → ${t.object}`)}return{content:[{type:"text",text:n.join("\n")}]}}catch(t){const e=t instanceof Error?t.message:String(t);return r.error(`concept_path error: ${e}`),{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}}),e("concept_search","Search concepts by label (fuzzy match). Returns matching concepts sorted by access frequency.",{query:n.string().describe("Search query — matches against concept labels"),limit:n.number().optional().describe("Max results (default 10)")},async t=>{try{const e=o.search(t.query,t.limit??10);if(0===e.length)return{content:[{type:"text",text:`No concepts matching "${t.query}".`}]};const n=e.map(t=>`- **${t.id}**: ${t.label} (accessed ${t.access_count}x, source: ${t.source})`);return{content:[{type:"text",text:`Found ${e.length} concepts:\n${n.join("\n")}`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return r.error(`concept_search error: ${e}`),{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}}),e("concept_stats","Get concept graph health statistics: total concepts, triples, orphans, never-accessed nodes, pending drafts, and top-accessed concepts.",{},async()=>{try{const t=o.stats(),e=[];if(e.push("### Concept Graph Stats"),e.push(`- Concepts: ${t.totalConcepts}`),e.push(`- Triples: ${t.totalTriples}`),e.push(`- Pending drafts: ${t.totalDraftsPending}`),e.push(`- Orphan concepts: ${t.orphanConcepts}`),e.push(`- Never accessed: ${t.neverAccessed}`),t.topAccessed.length>0){e.push(""),e.push("### Most accessed");for(const n of t.topAccessed)e.push(`- ${n.id}: ${n.label} (${n.access_count}x)`)}return{content:[{type:"text",text:e.join("\n")}]}}catch(t){const e=t instanceof Error?t.message:String(t);return r.error(`concept_stats error: ${e}`),{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}}),e("concept_draft","Add an observation to the concept draft staging area. Dreaming will process it into proper concepts and triples during the nightly cycle. Use this when you notice a new relationship, person, or fact worth adding to the knowledge graph.",{text:n.string().describe("The observation — describe the concept or relationship in natural language (e.g. 'Dott. Rossi is Caterina\\'s osteopath, seen on Feb 19')"),context:n.string().optional().describe("Optional context about the conversation where this came up")},async t=>{try{return{content:[{type:"text",text:`Draft #${o.addDraft(t.text,t.context)} saved. Will be processed by dreaming.`}]}}catch(t){const e=t instanceof Error?t.message:String(t);return r.error(`concept_draft error: ${e}`),{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}})]})}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Dynamic UI Tool Server
3
+ *
4
+ * Provides MCP tools for the agent to generate and manage Dynamic UI
5
+ * on connected nodes with Plasma capability.
6
+ *
7
+ * Tools:
8
+ * - dynamic_ui_list_nodes: List all capable nodes
9
+ * - dynamic_ui_render: Render Dynamic UI (HTML/CSS/JS + activities) on specific node
10
+ * - dynamic_ui_update: Send incremental JS update (maintains state, no reload)
11
+ * - dynamic_ui_clear: Clear Dynamic UI from node
12
+ */
13
+ import type { NodeRegistry } from "../gateway/node-registry.js";
14
+ export interface DynamicUIToolsDeps {
15
+ nodeRegistry: NodeRegistry;
16
+ /** Origin channel name (e.g. "webchat", "telegram") for routing actions back. */
17
+ channel?: string;
18
+ /** Origin chatId within the channel for routing actions back. */
19
+ chatId?: string;
20
+ }
21
+ /**
22
+ * Create Dynamic UI MCP tool server
23
+ */
24
+ export declare function createDynamicUIToolsServer(deps: DynamicUIToolsDeps): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
25
+ //# sourceMappingURL=dynamic-ui-tools.d.ts.map