beeos-claw 0.1.12 → 0.2.0

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 (76) hide show
  1. package/README.md +4 -4
  2. package/dist/a2a/cancel-handler.js +1 -0
  3. package/dist/a2a/mention-resolver.js +1 -0
  4. package/dist/a2a/poll-pending.js +1 -0
  5. package/dist/a2a/result-handler.js +1 -0
  6. package/dist/a2a/task-executor.js +1 -0
  7. package/dist/a2a/task-handle.js +1 -0
  8. package/dist/a2a/task-handler.js +1 -0
  9. package/dist/a2a/tools.js +1 -0
  10. package/dist/a2a-orchestrator.js +1 -0
  11. package/dist/a2a-orchestrator.test.js +1 -0
  12. package/dist/acp-gateway/bridge-core.js +1 -1
  13. package/dist/acp-gateway/gateway-events.js +1 -1
  14. package/dist/acp-gateway/history-replay.js +1 -1
  15. package/dist/acp-gateway/prompt-resource-resolver.js +1 -1
  16. package/dist/acp-gateway/session-state.js +1 -1
  17. package/dist/acp-gateway/session-state.test.js +1 -0
  18. package/dist/acp-gateway/tool-result-payload-strategies.js +1 -1
  19. package/dist/acp-gateway/transport.js +1 -1
  20. package/dist/acp-gateway/types.js +1 -1
  21. package/dist/acp-server.js +1 -1
  22. package/dist/agent-sync.js +1 -0
  23. package/dist/bridge-discovery.js +1 -0
  24. package/dist/canvas/a2ui-helpers.js +1 -0
  25. package/dist/canvas/action-router.js +1 -0
  26. package/dist/canvas/index.js +1 -0
  27. package/dist/canvas/relay-client.js +1 -0
  28. package/dist/canvas/state.js +1 -0
  29. package/dist/canvas/state.test.js +1 -0
  30. package/dist/canvas/tools.js +1 -0
  31. package/dist/canvas/ws-provider.js +1 -0
  32. package/dist/canvas-orchestrator.js +1 -0
  33. package/dist/channel-detection.js +1 -0
  34. package/dist/config.js +1 -1
  35. package/dist/device-identity.js +1 -0
  36. package/dist/discovery-cache.js +1 -0
  37. package/dist/errors/classify.js +1 -0
  38. package/dist/gateway-client.js +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/messaging/message-sdk.js +1 -0
  41. package/dist/surface/cron-resource.js +1 -0
  42. package/dist/{cron-schemas.js → surface/cron-schemas.js} +1 -1
  43. package/dist/surface/form-surface-builder.js +1 -0
  44. package/dist/surface/index.js +1 -0
  45. package/dist/surface/resource-surface-renderer.js +1 -0
  46. package/dist/terminal/bridge-handler.js +1 -0
  47. package/dist/terminal/index.js +1 -0
  48. package/dist/terminal/rpc.js +1 -0
  49. package/dist/terminal/websocket.js +1 -0
  50. package/dist/upload/file-stage.js +1 -0
  51. package/dist/upload/index.js +1 -0
  52. package/dist/{upload-result-cache.js → upload/result-cache.js} +1 -1
  53. package/dist/upload/tool.js +1 -0
  54. package/openclaw.plugin.json +32 -2
  55. package/package.json +7 -4
  56. package/dist/acp-gateway/local-session-history.js +0 -1
  57. package/dist/canvas-action-router.js +0 -1
  58. package/dist/canvas-api-client.js +0 -1
  59. package/dist/canvas-state.js +0 -1
  60. package/dist/canvas-tools.js +0 -1
  61. package/dist/canvas-ws-provider.js +0 -1
  62. package/dist/cron-resource.js +0 -1
  63. package/dist/file-stage.js +0 -1
  64. package/dist/form-surface-builder.js +0 -1
  65. package/dist/resource-surface-renderer.js +0 -1
  66. package/dist/terminal-bridge-handler.js +0 -1
  67. package/dist/terminal-rpc.js +0 -1
  68. package/dist/terminal-websocket.js +0 -1
  69. package/dist/upload-tool.js +0 -1
  70. /package/dist/{canvas-constants.js → canvas/constants.js} +0 -0
  71. /package/dist/{action-tool-generator.js → surface/action-tool-generator.js} +0 -0
  72. /package/dist/{form-schema-registry.js → surface/form-schema-registry.js} +0 -0
  73. /package/dist/{render-form-tool.js → surface/render-form-tool.js} +0 -0
  74. /package/dist/{resource-descriptor-registry.js → surface/resource-descriptor-registry.js} +0 -0
  75. /package/dist/{resource-descriptor.js → surface/resource-descriptor.js} +0 -0
  76. /package/dist/{terminal-session-manager.js → terminal/session-manager.js} +0 -0
@@ -7,9 +7,13 @@
7
7
  "type": "object",
8
8
  "additionalProperties": true,
9
9
  "properties": {
10
+ "agentGatewayUrl": {
11
+ "type": "string",
12
+ "description": "Base URL of the Agent Gateway API. Also readable from AGENT_GATEWAY_URL env var."
13
+ },
10
14
  "platformUrl": {
11
15
  "type": "string",
12
- "description": "Base URL of the BeeOS platform API (e.g. https://api.beeos.com)"
16
+ "description": "[Deprecated: use agentGatewayUrl] Legacy alias for agentGatewayUrl."
13
17
  },
14
18
  "canvasRelayUrl": {
15
19
  "type": "string",
@@ -77,7 +81,7 @@
77
81
  },
78
82
  "agentId": {
79
83
  "type": "string",
80
- "description": "Agent ID to use (default: main)"
84
+ "description": "Optional override for the OpenClaw agent ID used to route sessions. If omitted, automatically resolved from the Gateway agent list."
81
85
  }
82
86
  }
83
87
  },
@@ -114,6 +118,32 @@
114
118
  "description": "Directory for log files (default: ~/.openclaw/beeos-logs)"
115
119
  }
116
120
  }
121
+ },
122
+ "messageBus": {
123
+ "type": "object",
124
+ "description": "Message SDK configuration. Connects to Message Service (REST + Centrifugo WSS) for A2A messaging and real-time communication.",
125
+ "properties": {
126
+ "enabled": {
127
+ "type": "boolean",
128
+ "description": "Enable message bus. Auto-detected from serviceUrl or agentGatewayUrl. Also readable from BEEOS_MESSAGE_BUS_ENABLED env var."
129
+ },
130
+ "serviceUrl": {
131
+ "type": "string",
132
+ "description": "Message Service REST URL (e.g. http://localhost:8089). Also readable from MESSAGE_SERVICE_URL env var."
133
+ },
134
+ "url": {
135
+ "type": "string",
136
+ "description": "Override: Centrifugo WebSocket endpoint. Normally obtained from Message Service token response. Also readable from CENTRIFUGO_URL env var."
137
+ },
138
+ "principalId": {
139
+ "type": "string",
140
+ "description": "Override: messaging principal ID. Normally resolved from Gateway. Also readable from BEEOS_PRINCIPAL_ID env var."
141
+ },
142
+ "a2aEnabled": {
143
+ "type": "boolean",
144
+ "description": "Enable A2A tools (beeos_discover_agents, beeos_call_agent). Default: true"
145
+ }
146
+ }
117
147
  }
118
148
  }
119
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",
@@ -33,17 +33,20 @@
33
33
  "clean": "rm -rf dist",
34
34
  "build": "npm run clean && tsc -p tsconfig.json",
35
35
  "build:release": "npm run build && npm run minify",
36
- "minify": "find dist -name '*.js' -exec terser {} --compress drop_console=true,drop_debugger=true --mangle --output {} \\; && find dist -name '*.d.ts' -delete && find dist -name '*.d.ts.map' -delete"
36
+ "minify": "find dist -name '*.js' -exec terser {} --compress drop_console=true,drop_debugger=true --mangle --output {} \\; && find dist -name '*.d.ts' -delete && find dist -name '*.d.ts.map' -delete",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest"
37
39
  },
38
40
  "dependencies": {
39
41
  "@beeos-ai/bridge-client": "^0.1.0",
40
- "@beeos-ai/cli": "^0.0.2",
42
+ "centrifuge": "^5.5.3",
41
43
  "ws": "^8.18.0"
42
44
  },
43
45
  "devDependencies": {
44
46
  "@types/node": "^22.0.0",
45
47
  "@types/ws": "^8.18.1",
46
48
  "terser": "^5.39.0",
47
- "typescript": "^5.6.3"
49
+ "typescript": "^5.6.3",
50
+ "vitest": "^4.1.0"
48
51
  }
49
52
  }
@@ -1 +0,0 @@
1
- import{mkdirSync,existsSync,appendFileSync,openSync,readSync,closeSync,statSync}from"node:fs";import{join}from"node:path";import{homedir}from"node:os";const STATE_DIR=process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),HISTORY_DIR=join(STATE_DIR,"beeos-claw","session_history"),TAIL_CHUNK_BYTES=65536,TAIL_MAX_BYTES=1048576,TAIL_MAX_LINES=6400;function sessionFilePath(n){const e=n.replace(/[^a-zA-Z0-9._-]/g,"_");return join(HISTORY_DIR,`${e}.jsonl`)}function ensureDir(){existsSync(HISTORY_DIR)||mkdirSync(HISTORY_DIR,{recursive:!0})}export function appendHistoryEntry(n,e,t){try{ensureDir();const t=JSON.stringify(e)+"\n";appendFileSync(sessionFilePath(n),t,"utf8")}catch(e){t?.debug?.(`[local-history] write failed sessionKey=${n}: ${e instanceof Error?e.message:e}`)}}export function readLocalHistory(n,e=50){const t=sessionFilePath(n);if(!existsSync(t))return[];try{const n=readTailLines(t),r=[];for(const t of n){try{r.push(JSON.parse(t))}catch{}if(r.length>=e)break}return r.reverse()}catch{return[]}}function readTailLines(n){let e,t;try{e=statSync(n).size}catch{return[]}if(e<=0)return[];try{t=openSync(n,"r");let r=e,s=1048576,o="";const i=[];for(;r>0&&s>0&&i.length<6400;){const n=Math.min(65536,r,s),e=r-n,c=Buffer.allocUnsafe(n),a=readSync(t,c,0,n,e);if(a<=0)break;r=e,s-=a;const l=(c.toString("utf-8",0,a)+o).split("\n");o=l.shift()??"";for(let n=l.length-1;n>=0;n--){const e=l[n]?.trim();if(e&&(i.push(e),i.length>=6400))break}}const c=o.trim();return c&&i.length<6400&&i.push(c),i}catch{return[]}finally{if(void 0!==t)try{closeSync(t)}catch{}}}
@@ -1 +0,0 @@
1
- export class CanvasActionRouter{deps;lastActionTime=new Map;pendingActions=[];static DEBOUNCE_MS=300;static MAX_PENDING=5;constructor(t){this.deps=t}async route(t){const n=t.surfaceId??"",e=Date.now();if(e-(this.lastActionTime.get(n)??0)<CanvasActionRouter.DEBOUNCE_MS)return void this.deps.logger.debug?.(`[action-router] Debounced action on ${n}`);this.lastActionTime.set(n,e);const i=this.deps.stateManager.getActionHandler(n);if(i)try{await i(t),this.emitObs(t,"bound_handler")}catch(e){this.emitObs(t,"bound_handler_error"),this.deps.logger.warn?.(`[action-router] Bound handler for ${n} threw: ${e instanceof Error?e.message:e}`)}else this.emitObs(t,"agent_forward"),await this.deps.forwardToAgent(t)}enqueue(t){this.pendingActions.length>=CanvasActionRouter.MAX_PENDING&&this.pendingActions.shift(),this.pendingActions.push(t)}async drainPending(){if(0===this.pendingActions.length)return;const t=this.deduplicatePending();this.pendingActions=[];for(const n of t)await this.deps.forwardToAgent(n)}clearPending(){this.pendingActions=[]}reset(){this.lastActionTime.clear(),this.pendingActions=[]}deduplicatePending(){if(this.pendingActions.length<=1)return[...this.pendingActions];const t=new Map;for(const n of this.pendingActions)t.set(n.surfaceId??"",n);return Array.from(t.values())}emitObs(t,n){this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.action_routed",severity:n.endsWith("_error")?"warn":"info",payload:{surfaceId:t.surfaceId,actionName:t.userAction?.name,resolution:n}})}}export function formatActionForAgent(t,n){const e=t.surfaceId??"unknown",i=t.userAction?.name??"unknown",s=t.userAction?.context;let o=`[Canvas Interaction] The user clicked "${i}" on surface "${e}"`;if(n?.title&&(o+=` ("${n.title}")`),o+=".",n?.types?.length&&(o+=` Surface contains: ${n.types.join(", ")}.`),s&&Object.keys(s).length>0){const t=JSON.stringify(s);o+=`\nAction context: ${t.length>2e3?t.slice(0,2e3)+"…(truncated)":t}`}return o+=`\nRespond to this interaction. If the surface needs updating, use render_ui with id="${e}".`,o}
@@ -1 +0,0 @@
1
- import{agentFetch}from"./agent-auth.js";export class CanvasApiClient{baseUrl;keys;constructor(s){this.baseUrl=s.platformUrl.replace(/\/+$/,""),this.keys=s.agentKeys}async request(s,a,t){const e=await agentFetch(`${this.baseUrl}${a}`,this.keys,{method:s,headers:{"Content-Type":"application/json"},body:t?JSON.stringify(t):void 0}),n=await e.json();if(!n.success)throw new Error(n.error??`Canvas API ${s} ${a} failed: ${e.status}`);return n.data??null}async getCanvasForSession(s){try{return await this.request("GET",`/api/v1/canvas-session/${s}`)}catch(s){const a=s instanceof Error?s.message:String(s);if(a.includes("404")||a.includes("not found")||a.includes("null"))return null;throw s}}async createCanvas(s){const a=await this.request("POST","/api/v1/canvas/agent",{title:s?.title??"Agent Canvas",sessionId:s?.sessionId});if(!a)throw new Error("Canvas creation returned no data");return a}async linkChat(s,a){await this.request("POST",`/api/v1/canvas/${s}/link`,{sessionId:a})}async ensureCanvasForSession(s){const a=await this.getCanvasForSession(s);if(a)return a.canvasId;return(await this.createCanvas({sessionId:s})).id}async createSnapshot(s,a){return this.request("POST",`/api/v1/canvas/${s}/snapshots`,{description:a??"Pre-agent edit"})}}
@@ -1 +0,0 @@
1
- import{CanvasWSProvider}from"./canvas-ws-provider.js";import{MSG_SYNC,MSG_SYNC_UPDATE}from"./canvas-constants.js";const MAX_SURFACES_PER_SESSION=20,MAX_STATE_MAP_BYTES=2097152,MAX_COMPONENTS_PER_SURFACE=500,RATE_LIMIT_PER_SURFACE_PER_SEC=30;export class CanvasStateManager{surfaces=new Map;actionHandlers=new Map;transport=null;activeCanvasId=null;lastResolvedSessionId=null;relayOpts=null;transportFactory;setRelayOpts(e){this.relayOpts=e}setTransportFactory(e){this.transportFactory=e}createTransport(e){return this.transportFactory?this.transportFactory(e):new CanvasWSProvider(e)}connectForCanvas(e,t){if(this.activeCanvasId===e&&this.transport?.connected)return void(t&&(this.lastResolvedSessionId=t));if(!this.relayOpts)return;this.transport&&this.transport.disconnect(),this.activeCanvasId&&this.activeCanvasId!==e&&this.clear(),this.activeCanvasId=e,this.lastResolvedSessionId=t??null,this.transport=this.createTransport(this.relayOpts);const s=this.relayOpts.logger;this.transport.connect(e,{onUpdate:e=>{s?.debug?.(`[canvas-state] Received update from relay: ${e.length} bytes`),this.processIncomingRelayMessage(e)},onAwareness:e=>{s?.debug?.(`[canvas-state] Received awareness from relay: ${e.length} bytes`)}})}disconnectFromCanvasService(){this.transport&&(this.transport.disconnect(),this.transport=null),this.activeCanvasId=null,this.lastResolvedSessionId=null}get canvasServiceConnected(){return null!==this.activeCanvasId&&null!==this.transport}get currentCanvasId(){return this.activeCanvasId}get currentSessionId(){return this.lastResolvedSessionId}syncDeleteToRelay(e){if(this.transport)try{const t=JSON.stringify({surfaceId:e,deleted:!0}),s=(new TextEncoder).encode(t),r=new Uint8Array(2+s.length);r[0]=MSG_SYNC,r[1]=MSG_SYNC_UPDATE,r.set(s,2),this.transport.sendUpdate(r)}catch{}}syncSurfaceToRelay(e,t,s){if(this.transport)try{const r=JSON.stringify({surfaceId:e,components:t,presentation:s??"panel"}),a=(new TextEncoder).encode(r),n=new Uint8Array(2+a.length);n[0]=MSG_SYNC,n[1]=MSG_SYNC_UPDATE,n.set(a,2),this.transport.sendUpdate(n),this.relayOpts?.logger?.info?.(`[canvas-state] Queued/sent ${n.length} bytes to relay for surfaceId=${e}`)}catch(e){this.relayOpts?.logger?.warn?.(`[canvas-state] syncSurfaceToRelay failed: ${e}`)}}processIncomingRelayMessage(e){if(!(e.length<3)&&e[0]===MSG_SYNC&&e[1]===MSG_SYNC_UPDATE)try{const t=(new TextDecoder).decode(e.subarray(2)),s=JSON.parse(t);if(!s.surfaceId)return;s.deleted&&(this.removeSurface(s.surfaceId),this.relayOpts?.logger?.info?.(`[canvas-state] Surface ${s.surfaceId} deleted by remote participant`))}catch{}}has(e){return this.surfaces.has(e)}get(e){return this.surfaces.get(e)}getAll(){return Array.from(this.surfaces.values())}getAllActive(){return Array.from(this.surfaces.values())}create(e,t,s,r){this.enforceMaxSurfaces();const a={surfaceId:e,presentation:t,components:s,dataModels:new Map,origin:r?.origin??{kind:"agent"},createdAt:Date.now(),updatedAt:Date.now(),tokenBucket:{tokens:30,lastRefill:Date.now()}};return this.surfaces.set(e,a),r?.actionHandler&&this.actionHandlers.set(e,r.actionHandler),this.enforceMemoryBudget(),this.syncSurfaceToRelay(e,s,t),a}update(e,t){const s=this.surfaces.get(e);if(s)return s.components=t,s.updatedAt=Date.now(),this.enforceMemoryBudget(),this.syncSurfaceToRelay(e,t,s.presentation),s}delete(e){const t=this.removeSurface(e);return t&&this.syncDeleteToRelay(e),t}clear(){this.actionHandlers.clear(),this.surfaces.clear()}get size(){return this.surfaces.size}getActionHandler(e){return this.actionHandlers.get(e)}bindActionHandler(e,t){this.actionHandlers.set(e,t)}summarize(e){const t=this.surfaces.get(e);if(!t||!Array.isArray(t.components))return null;const s=t.components,r=[...new Set(s.map(e=>e.type).filter(Boolean))],a=s.find(e=>e.properties?.title);return{types:r,title:a?.properties?.title}}checkRateLimit(e){const t=this.surfaces.get(e);if(!t)return!0;const s=Date.now(),r=(s-t.tokenBucket.lastRefill)/1e3;return t.tokenBucket.tokens=Math.min(30,t.tokenBucket.tokens+30*r),t.tokenBucket.lastRefill=s,t.tokenBucket.tokens>=1&&(t.tokenBucket.tokens-=1,!0)}validateComponentCount(e){return!Array.isArray(e)||e.length<=500}removeSurface(e){return this.actionHandlers.delete(e),this.surfaces.delete(e)}enforceMaxSurfaces(){if(this.surfaces.size<20)return;let e,t=1/0;for(const[s,r]of this.surfaces)r.createdAt<t&&(t=r.createdAt,e=s);e&&this.removeSurface(e)}enforceMemoryBudget(){let e=0;for(const[,t]of this.surfaces)e+=this.estimateSize(t);if(e<=2097152)return;const t=Array.from(this.surfaces.entries()).sort(([,e],[,t])=>e.updatedAt-t.updatedAt);for(const[s,r]of t){if(e<=2097152)break;e-=this.estimateSize(r),this.removeSurface(s)}}estimateSize(e){try{let t=2*JSON.stringify(e.components).length;if(e.dataModels.size>0)for(const s of e.dataModels.values())t+=2*JSON.stringify(s).length;return t}catch{return 1024}}}
@@ -1 +0,0 @@
1
- function safeStr(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if("object"==typeof e){const t=e;for(const e of["value","label","name","text","title"])if("string"==typeof t[e]||"number"==typeof t[e])return String(t[e]);try{return JSON.stringify(e)}catch{return"[Object]"}}return String(e)}let componentIdCounter=0;function nextCid(){return`c_${++componentIdCounter}_${Date.now().toString(36)}`}const EMBEDDABLE_DOMAINS=[/youtube\.com\/(?:watch|embed|shorts)/i,/youtu\.be\//i,/vimeo\.com\//i,/dailymotion\.com\//i,/google\.com\/maps/i,/maps\.google\./i,/openstreetmap\.org/i,/codepen\.io\//i,/codesandbox\.io\//i,/stackblitz\.com\//i,/figma\.com\/(?:file|embed|proto|design)/i,/miro\.com\/app\/board/i,/spotify\.com\/(?:embed|track|album|playlist)/i,/soundcloud\.com\//i,/twitter\.com\/.*\/status/i,/x\.com\/.*\/status/i,/loom\.com\/share/i,/notion\.so\//i,/airtable\.com\//i,/excalidraw\.com\//i,/canva\.com\/design/i,/docs\.google\.com\//i,/drive\.google\.com\//i,/slides\.google\.com\//i,/sheets\.google\.com\//i,/github\.com\/.*\/blob/i,/gist\.github\.com\//i,/jsfiddle\.net\//i,/replit\.com\//i,/observable\.com\//i,/datawrapper\.dwcdn\.net/i,/grafana\./i,/metabase\./i,/retool\./i,/streamlit\./i,/huggingface\.co\/spaces/i];function isEmbeddableUrl(e){return!("string"!=typeof e||!e)&&EMBEDDABLE_DOMAINS.some(t=>t.test(e))}function translateComponent(e){if(!e||"object"!=typeof e)return[{id:nextCid(),type:"Text",properties:{text:"[invalid component]"}}];const t=nextCid(),a="string"==typeof e.type?e.type.toLowerCase():"text";switch(a){case"card":return buildCard(t,e);case"table":return[buildTable(t,e)];case"form":return[buildForm(t,e)];case"text":return[{id:t,type:"Text",properties:{text:String(e.content??"")}}];case"image":return[{id:t,type:"Image",properties:{src:String(e.url??""),alt:String(e.alt??"")}}];case"button":return[{id:t,type:"Button",properties:{label:String(e.label??"Button"),actionId:String(e.actionId??t)}}];case"grid":{const a=(Array.isArray(e.children)?e.children:Array.isArray(e.components)?e.components:[]).flatMap(e=>translateComponent(e));return[{id:t,type:"Grid",properties:{columns:e.columns??2,gap:e.gap??"md",responsive:!1!==e.responsive},children:a.filter((e,t)=>t===a.findIndex(e=>e.id===a[t]?.id)).map(e=>e.id)},...a]}case"row":case"column":return(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));case"web_embed":return[{id:t,type:"WebEmbed",properties:{url:e.url,title:e.title,aspectRatio:e.aspectRatio??"16:9"}}];case"video":case"map":case"file_preview":case"audio":return[{id:t,type:"WebEmbed",properties:{url:e.url,title:e.title,aspectRatio:"16:9"}}];case"chart":return[{id:t,type:"Chart",properties:{chartType:e.chartType,title:e.title,labels:e.labels,datasets:e.datasets,value:e.value,max:e.max,stages:e.stages}}];case"code":return[{id:t,type:"CodeBlock",properties:{language:String(e.language??""),code:String(e.code??"")}}];case"markdown":return[{id:t,type:"Markdown",properties:{content:String(e.content??"")}}];case"tabs":{const a=Array.isArray(e.tabs)?e.tabs:Array.isArray(e.children)?e.children:Array.isArray(e.items)?e.items:[];if(0===a.length)return[{id:t,type:"Text",properties:{text:safeStr(e.title??e.label??"")}}];const r=[];for(const e of a){const t=safeStr(e.label??e.title??e.id??"");t&&r.push({id:nextCid(),type:"Markdown",properties:{content:`### ${t}`}});const a=Array.isArray(e.children)?e.children:[];r.push(...a.flatMap(e=>translateComponent(e)))}return r}case"accordion":{const a=Array.isArray(e.items)?e.items:Array.isArray(e.children)?e.children:[];return a.length>0?a.flatMap(e=>[{id:nextCid(),type:"Text",properties:{text:safeStr(e.title??e.label??""),variant:"heading"}},...Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[]]):[{id:t,type:"Text",properties:{text:safeStr(e.title??"")}}]}case"alert":return[{id:t,type:"Alert",properties:{variant:e.variant??"info",title:e.title,message:e.message,dismissible:e.dismissible??!1}}];case"progress":{const a=Number(e.value??0),r=Number(e.max??100),n=r>0?Math.round(a/r*100):0;return[{id:t,type:"Text",properties:{text:`${safeStr(e.label)||"Progress"}: ${n}% (${a}/${r})`}}]}case"stat":return[{id:t,type:"Stat",properties:{label:e.label,value:e.value,change:e.change,changeType:e.changeType,description:e.description,icon:e.icon}}];case"badge":return[{id:t,type:"Text",properties:{text:safeStr(e.text??e.label)}}];case"divider":return[{id:t,type:"Text",properties:{text:safeStr(e.label)||"---"}}];case"rating":return[{id:t,type:"Text",properties:{text:`Rating: ${String(e.value??0)}/${String(e.max??5)}`}}];case"timeline":{const a=(Array.isArray(e.items)?e.items:[]).map(e=>`${safeStr(e.time??e.date??"")} — ${safeStr(e.title??"")}${e.description?`: ${safeStr(e.description)}`:""}`);return[{id:t,type:"Markdown",properties:{content:(e.title?`**${safeStr(e.title)}**\n`:"")+a.join("\n")}}]}case"avatar":return[{id:t,type:"Text",properties:{text:safeStr(e.name??"")}}];case"callout":return[{id:t,type:"Alert",properties:{variant:"tip"===e.variant?"success":"warning"===e.variant||"caution"===e.variant?"warning":"important"===e.variant?"error":"info",title:e.title,message:e.content??""}}];case"list":return[{id:t,type:"List",properties:{title:e.title,items:e.items??[],ordered:e.ordered??!1,variant:e.variant}}];case"json_viewer":{let a;try{a="string"==typeof e.data?e.data:JSON.stringify(e.data,null,2)}catch{a=String(e.data??"")}return[{id:t,type:"Markdown",properties:{content:`${e.title?`**${safeStr(e.title)}**\n`:""}\`\`\`json\n${a}\n\`\`\``}}]}case"carousel":return[{id:t,type:"List",properties:{title:e.title,items:e.items??[]}}];case"key_value":return[{id:t,type:"KeyValue",properties:{title:e.title,items:e.items??[],variant:e.variant}}];case"link":return isEmbeddableUrl(e.url)?[{id:t,type:"WebEmbed",properties:{url:e.url,title:String(e.text??e.label??""),aspectRatio:"16:9"}}]:[{id:t,type:"Link",properties:{text:e.text??e.label,url:e.url,variant:e.variant??"default"}}];default:return[{id:t,type:"Text",properties:{text:`[Unsupported: ${a}]`}}]}}function buildCard(e,t){const a={};t.title&&(a.title=safeStr(t.title)),t.subtitle&&(a.subtitle=safeStr(t.subtitle)),t.description&&(a.description=safeStr(t.description)),t.image&&(a.image=safeStr(t.image)),t.icon&&(a.icon=safeStr(t.icon)),t.badge&&(a.badge=safeStr(t.badge)),t.badgeVariant&&(a.badgeVariant=safeStr(t.badgeVariant)),t.footer&&(a.footer=safeStr(t.footer)),t.url&&(a.url=safeStr(t.url)),t.layout&&(a.layout=safeStr(t.layout));const r=Array.isArray(t.actions)?t.actions:[];r.length>0&&(a.actions=r.filter(e=>e&&"object"==typeof e).map(e=>({label:safeStr(e.label??"Action"),actionId:safeStr(e.actionId??e.id??nextCid()),variant:e.variant})));const n=[],i=Array.isArray(t.fields)?t.fields:[];for(const e of i)e&&"object"==typeof e&&n.push({id:nextCid(),type:"Text",properties:{text:`${safeStr(e.label)}: ${safeStr(e.value)}`}});const s={id:e,type:"Card",properties:a};return n.length>0?(s.children=n.map(e=>e.id),[s,...n]):[s]}function buildTable(e,t){const a=Array.isArray(t.columns)?t.columns:[],r=Array.isArray(t.rows)?t.rows:[],n=[],i=[];for(const e of a)if("string"==typeof e)n.push(e),i.push(e);else if(e&&"object"==typeof e){const t=e,a=safeStr(t.key??t.id??t.field??t.name??t.label??""),r=safeStr(t.label??t.title??t.header??t.name??a);n.push(r),i.push(a)}else{const t=safeStr(e);n.push(t),i.push(t)}const s=r.map(e=>{if(Array.isArray(e))return e;if(e&&"object"==typeof e&&i.length>0){const t=e;return i.map(e=>{const a=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=a?t[a]:""})}return e});return{id:e,type:"Table",properties:{title:safeStr(t.title),columns:n,rows:s}}}function buildForm(e,t){return{id:e,type:"Form",properties:{title:safeStr(t.title),description:t.description,fields:t.fields??[],submitLabel:safeStr(t.submitLabel)||"Submit",submitActionId:t.submitActionId??`${e}_submit`}}}const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1",EXTENDED_TYPES=new Set(["WebEmbed","Chart","CodeBlock","Markdown","Alert","Stat","List","KeyValue","Link","Grid"]);function hasExtendedComponent(e){return e.some(e=>EXTENDED_TYPES.has(e.type))}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render structured visual content to the Canvas panel. ALWAYS prefer this tool over the canvas tool for displaying UI. Use for charts, tables, stats, key-value pairs, lists, cards, forms, alerts, code, markdown, images, links, web embeds — NOT for text paragraphs. When you call this tool, keep your text reply brief.";export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","components"],properties:{id:{type:"string",description:"Descriptive, human-readable identifier (e.g., 'sales-report', 'user-dashboard'). Reuse same id to update an existing surface."},components:{type:["array","string"],description:"UI components to render",items:{type:"object",required:["type"],properties:{type:{type:"string",enum:["card","table","stat","key_value","list","grid","chart","markdown","code","text","image","web_embed","alert","form","button","link"]},title:{type:"string"},content:{type:"string"},label:{type:"string"},description:{type:"string"},url:{type:"string"},image:{type:"string",description:"Image URL for card header or list item thumbnail"},subtitle:{type:"string",description:"Card subtitle or list item subtitle"},badge:{type:"string",description:"Card/list badge text (e.g. price, status)"},badgeVariant:{type:"string",enum:["default","success","warning","error","info"]},footer:{type:"string",description:"Card footer text"},layout:{type:"string",enum:["vertical","horizontal"],description:"Card layout: vertical (image top) or horizontal (image left)"},actions:{type:"array",description:"Card action buttons: [{label, actionId, variant?}]"},fields:{type:"array",description:"For card: [{label,value}]. For form: [{name,label,inputType,options,...}]"},children:{type:"array",description:"For grid: nested child components (card, stat, image, etc.)"},gap:{type:"string",enum:["sm","md","lg"],description:"Grid gap size"},columns:{type:"array",description:'Column headers or grid column count (1-6). Table: ["Name","Status"]. Grid: number.'},rows:{type:"array",description:'Row data as arrays matching column order, e.g. [["prod-1", "running"]]'},submitLabel:{type:"string"},submitActionId:{type:"string"},actionId:{type:"string"},alt:{type:"string"},aspectRatio:{type:"string",enum:["16:9","4:3","1:1"]},chartType:{type:"string",enum:["bar","horizontal_bar","line","area","scatter","pie","doughnut","radar","gauge","candlestick","funnel","sparkline"]},stages:{type:"array",description:"For funnel chart: [{label,value}]"},labels:{type:"array"},datasets:{type:"array"},language:{type:"string"},code:{type:"string"},items:{type:"array",description:"For list: [{title,subtitle,description,image,icon,badge,value,url}]. For key_value: [{key,value,copyable}]"},variant:{type:"string",description:"Alert: info/success/warning/error. Link: default/button/subtle"},message:{type:"string"},value:{type:"number"},max:{type:"number"},change:{type:"string"},changeType:{type:"string",enum:["positive","negative","neutral"]},icon:{type:"string"},text:{type:"string"}}}}}};export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};async function ensureRelayConnected(e,t){const{stateManager:a,canvasApiClient:r,logger:n}=e;if((!a.canvasServiceConnected||a.currentSessionId!==t)&&r)try{const e=await r.ensureCanvasForSession(t);a.connectForCanvas(e,t),n.info?.(`[canvas] Relay connected for canvas=${e} session=${t}`)}catch(e){n.warn?.(`[canvas] Failed to connect relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:a,writeObs:r,isCanvasActive:n}=e;let i=0;return async(s,o)=>{if(n&&!n())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const c=o,l=c.id,d="panel";let p=c.components;if("string"==typeof p)try{p=JSON.parse(p)}catch{}if(!l||!Array.isArray(p)||!p.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return a.warn?.(`[canvas] Rate limited surfaceId=${l}`),r({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const u=p.flatMap(translateComponent),m=t.has(l),g=hasExtendedComponent(u)?BEEOS_EXTENDED_CATALOG:void 0,y=e.getActiveSessionId?.(s);if(!y)return a.warn?.(`[canvas] No active session for toolCallId=${s}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};await ensureRelayConnected(e,y);const f=Date.now();e.canvasApiClient&&t.canvasServiceConnected&&t.currentCanvasId&&f-i>6e4&&e.canvasApiClient.createSnapshot(t.currentCanvasId,"Pre-agent edit").then(()=>{i=Date.now(),a.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>a.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),m?t.update(l,u):t.create(l,d,u,{origin:{kind:"agent"}}),e.sendCanvasUpdate(y,{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:l,presentation:d,message:{surfaceUpdate:{surfaceId:l,components:u,catalogId:g}}});const b=p.map(e=>e.type);r({component:"canvas",domain:"canvas",name:m?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:b}}),a.debug?.(`[canvas] ${m?"Updated":"Created"} surface=${l} components=${p.length}`);return{success:!0,surfaceId:l,presentation:d,isUpdate:m,activeSurfaces:t.getAllActive().map(e=>e.surfaceId)}}}export function createCloseUiHandler(e){const{stateManager:t,logger:a,writeObs:r}=e;return async(n,i)=>{const s=i.id;if(!s)return{success:!1,error:"id is required"};const o=t.get(s),c=o?Date.now()-o.createdAt:0;t.delete(s);const l=e.getActiveSessionId?.(n);return l?(e.sendCanvasUpdate(l,{sessionUpdate:"canvas.reset",surfaceId:s}),r({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:s,lifetime_ms:c}}),a.debug?.(`[canvas] Closed surface=${s}`),{success:!0,surfaceId:s,message:"UI closed"}):(a.warn?.(`[canvas] No active session for toolCallId=${n}, close_ui local-only`),{success:!0,surfaceId:s,message:"UI closed (local only)"})}}export function buildCanvasActionSystemMessage(e){const t=e.userAction;return{type:"canvas_user_action",surfaceId:e.surfaceId,actionName:t?.name??"unknown",sourceComponentId:t?.sourceComponentId??"",context:t?.context??{}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.** The `render_ui` tool is the preferred and optimized way to render UI in the Canvas panel.\n\nRules:\n- When using render_ui, keep your chat reply to ONE short sentence. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Do NOT put large text blocks in render_ui — that belongs in chat.\n- Only use `grid` as a layout container. Keep nesting to grid > card/stat/image — max 1 level deep. Otherwise list components sequentially (they stack vertically).\n- Prefer fewer, larger components. One well-structured table is better than many small cards.\n- Canvas renders in a narrow sidebar (~400px). Use markdown headings (### Section) to organize sections.\n- Use grid + card for product showcases, dashboards, feature grids. Use list for compact item lists.\n\n**Surface IDs**: Use descriptive, human-readable IDs (e.g., "sales-report", "user-dashboard"). Never use generic IDs like "ui1" or random strings.\n\n**Update vs Create**: Same `id` updates existing surface; different `id` creates new one.\nWhen the user asks to modify, adjust, fix, or update existing canvas content, you MUST reuse the original surface id. Check [Canvas State] in the message for active surface ids and their content. Only create a new surface when showing genuinely new/different content.\n\n### Component Types\n\n**Layout**: grid (columns:1-6, gap:sm/md/lg, children:[...]) — responsive grid container for card/stat/image\n\n**Data Display**: card (image, title, subtitle, description, icon, badge, badgeVariant, footer, url, layout:vertical/horizontal, actions:[{label,actionId}]), table (columns, rows), stat (label, value, change, changeType, icon), key_value (items:[{key,value,copyable}]), list (items:[{title,subtitle,description,image,icon,badge,value,url}])\n\n**Chart**: chart (chartType, labels, datasets) — chartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge (value,max), candlestick, funnel (stages:[{label,value}]), sparkline\n\n**Text & Code**: text (content), markdown (content), code (language, code)\n\n**Media**: image (url, alt), web_embed (url, aspectRatio — embed ANY web page/app as iframe: YouTube, Maps, Figma, dashboards, docs, tools, etc.)\n\n**Feedback**: alert (variant:info/success/warning/error, title, message)\n\n**Interactive**: form (fields:[{name,label,inputType,options,placeholder,required}]), button (label, actionId), link (text, url, variant:default/button/subtle)\n\n**web_embed vs link**: Use `web_embed` when the URL should be displayed inline as embedded content. Use `link` only for plain navigation.\n\n### Composition Patterns\n- **Product showcase**: grid(columns:3) containing card(image, title, description, badge:"$29", actions:[{label:"Buy"}]) × N\n- **Dashboard KPIs**: grid(columns:3) containing stat × N\n- **Image gallery**: grid(columns:3) containing image × N\n- **Feature grid**: grid(columns:2) containing card(icon, title, description) × N\n- **Compact item list**: list(items:[{image, title, subtitle, value:"$29", badge:"New", url}])\n- **Article card**: card(image, title, description, url, layout:"horizontal")\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING="[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content (charts, tables, stats, key-value, lists, cards, grids, alerts, forms, links, embeds, images, code, markdown). Use grid(columns, children:[card/stat/image]) for multi-column layouts like product showcases and dashboards. Card supports: image, title, subtitle, description, icon, badge, footer, url, actions, layout:horizontal/vertical. List items support: image, title, subtitle, description, badge, value, url. Charts: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline. Use web_embed for embedding URLs as iframes. Use link for navigation. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.";export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e){const t=e.getAllActive();if(0===t.length)return null;const a=[...t].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),r=t.length-a.length;let n=`[Canvas State] Active surfaces: ${a.map(t=>{const a=e.summarize(t.surfaceId);if(!a)return`"${t.surfaceId}"`;const r=a.types.slice(0,3).join(", "),n=a.title?`: "${a.title}"`:"";return`"${t.surfaceId}" (${r}${n})`}).join("; ")}.`;return r>0&&(n+=` ...and ${r} more.`),n+=" To modify existing content, reuse its id in render_ui.",n.length>500&&(n=n.slice(0,497)+"..."),n}
@@ -1 +0,0 @@
1
- import WebSocket from"ws";import{agentAuthHeaders}from"./agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./canvas-constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(s){this.opts=s}connect(s,e){this.canvasId=s,this.onUpdate=e.onUpdate,this.onAwareness=e.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const s=`/ws/agent/${this.canvasId}`,e=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${s}`;this.opts.logger?.info?.(`[canvas-ws] Connecting to ${e}`);const t=agentAuthHeaders("GET",s,this.opts.agentKeys),n=new WebSocket(e,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const s of this.pendingMessages)n.send(s);this.pendingMessages=[]}),n.on("message",s=>{const e=new Uint8Array(s instanceof ArrayBuffer?s:s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength));if(0===e.length)return;const t=e[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(e):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(e)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",s=>{this.opts.logger?.warn?.("[canvas-ws] Error:",s.message),n.close()})}scheduleReconnect(){if(this.closed)return;const s=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${s}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},s)}sendUpdate(s){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(s):this.pendingMessages.push(s)}sendAwareness(s){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(s):this.pendingMessages.push(s)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
@@ -1 +0,0 @@
1
- function formatSchedule(e,t){const a=e;if(!a)return"—";const r=a.kind;if("cron"===r)return a.expr??"—";if("every"===r){const e=a.everyMs??0;return e>=864e5?`Every ${Math.round(e/864e5)}d`:e>=36e5?`Every ${Math.round(e/36e5)}h`:e>=6e4?`Every ${Math.round(e/6e4)}m`:`Every ${Math.round(e/1e3)}s`}return"at"===r?`At ${a.at??"—"}`:"—"}export const CRON_RESOURCE={domain:"cron",title:"Scheduled Tasks",listRpc:"cron.list",listParams:{includeDisabled:!0,limit:50},extractItems:e=>e?.jobs??[],itemId:e=>e.id,columns:[{key:"name",label:"Name"},{key:"schedule",label:"Schedule",format:formatSchedule},{key:"enabled",label:"Status",format:e=>e?"Enabled":"Disabled"},{key:"state.lastRunStatus",label:"Last Run",format:e=>String(e??"—")},{key:"state.nextRunAtMs",label:"Next Run",format:e=>e?new Date(Number(e)).toLocaleString():"—"}],itemActions:[{id:"toggle",type:"rpc",rpc:"cron.update",label:e=>e.enabled?`Disable "${e.name}"`:`Enable "${e.name}"`,buildParams:e=>({id:e.id,patch:{enabled:!e.enabled}}),description:"Toggle a cron job on/off"},{id:"edit",type:"form",formDomain:"cron",formOperation:"update",label:e=>`Edit "${e.name}"`,buildFormData:e=>e},{id:"run",type:"rpc",rpc:"cron.run",label:e=>`Run "${e.name}"`,buildParams:e=>({id:e.id,mode:"force"}),description:"Run a cron job immediately"},{id:"delete",type:"rpc",rpc:"cron.remove",label:e=>`Delete "${e.name}"`,buildParams:e=>({id:e.id}),description:"Delete a cron job"}],globalActions:[{id:"refresh",type:"refresh",label:"Refresh"}],slashCommand:"cron",emptyTitle:"No scheduled tasks",emptyContent:"Use the agent to create cron jobs, or add them via the form below."};
@@ -1 +0,0 @@
1
- import{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";import{Errors,sendHttpError}from"./errors/index.js";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),UPLOAD_DIR=join(OPENCLAW_HOME,"workspace","uploads"),DOWNLOAD_TIMEOUT_MS=3e5;function classifyDownloadError(r,e){if(r instanceof Error&&"AbortError"===r.name)return{message:"download timed out",category:"timeout",retriable:!0};if(e)return 429===e||e>=500?{message:`HTTP ${e}`,category:"http_5xx",retriable:!0,httpStatus:e}:e>=400?{message:`HTTP ${e}`,category:"http_4xx",retriable:!1,httpStatus:e}:{message:`HTTP ${e}`,category:"invalid_response",retriable:!1,httpStatus:e};const t=r instanceof Error?r.message:String(r),o=r.code;return"ENOSPC"===o||"EACCES"===o||"EROFS"===o?{message:t,category:"fs_error",retriable:!1}:{message:t,category:"network_error",retriable:!0}}export async function handleFileStage(r,e,t){const o=r,a=e,s=getHeader(o,"x-beeos-token")||getHeader(o,"authorization")?.replace(/^Bearer\s+/i,""),n=process.env.BRIDGE_INTERNAL_SECRET;if(n&&s!==n)return void sendHttpError(a,401,Errors.Unauthorized);let i;try{i=await readJSONBody(o)}catch{return void sendHttpError(a,400,Errors.InvalidBody)}if(!i.fileId||!i.fileName||!i.presignedUrl)return void sendHttpError(a,400,Errors.MissingField,{message:"fileId, fileName, and presignedUrl are required"});const c=i.targetDir?join(OPENCLAW_HOME,"workspace",i.targetDir):UPLOAD_DIR,d=i.fileName.replace(/[/\\:*?"<>|]/g,"_"),l=`${i.fileId}_${d}`,f=join(c,l);try{return await access(f,constants.F_OK),void sendJSON(a,200,{ok:!0,localPath:f,cached:!0})}catch{const r=await findByFileIdPrefix(c,i.fileId);if(r)return void sendJSON(a,200,{ok:!0,localPath:r,cached:!0})}try{await mkdir(c,{recursive:!0})}catch(r){return void sendHttpError(a,500,Errors.FsError,{message:`Failed to create target directory: ${r instanceof Error?r.message:String(r)}`})}const u=new AbortController,p=setTimeout(()=>u.abort(),3e5);"object"==typeof p&&"unref"in p&&p.unref();try{const r=await fetch(i.presignedUrl,{signal:u.signal});if(!r.ok||!r.body){const e=classifyDownloadError(null,r.status);return void sendHttpError(a,502,Errors.DownloadFailed,{message:`Download failed: ${e.message}`,extra:{errorCategory:e.category,retriable:e.retriable,httpStatus:e.httpStatus}})}const e=createWriteStream(f);await pipeline(Readable.fromWeb(r.body),e)}catch(r){const e=classifyDownloadError(r);return void sendHttpError(a,502,Errors.DownloadFailed,{message:`Download failed: ${e.message}`,extra:{errorCategory:e.category,retriable:e.retriable}})}finally{clearTimeout(p)}sendJSON(a,200,{ok:!0,localPath:f,cached:!1})}function getHeader(r,e){const t=r.headers[e];return Array.isArray(t)?t[0]:t}function readJSONBody(r){return new Promise((e,t)=>{const o=[];r.on("data",r=>{o.push(Buffer.isBuffer(r)?r:Buffer.from(r))}),r.on("end",()=>{try{e(JSON.parse(Buffer.concat(o).toString("utf-8")))}catch(r){t(r)}}),r.on("error",t)})}function sendJSON(r,e,t){r.statusCode=e,r.setHeader("Content-Type","application/json"),r.end(JSON.stringify(t))}async function findByFileIdPrefix(r,e){try{const t=await readdir(r),o=`${e}_`;for(const e of t)if(e.startsWith(o)){const t=join(r,e);try{return await access(t,constants.R_OK),t}catch{continue}}}catch{}return null}
@@ -1 +0,0 @@
1
- const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1";function getByPath(e,t){if(!e)return;const a=t.split(".");let s=e;for(const e of a){if(null==s||"object"!=typeof s)return;s=s[e]}return s}function setByPath(e,t,a){const s=t.split(".");let i=e;for(let e=0;e<s.length-1;e++){const t=s[e];null!=i[t]&&"object"==typeof i[t]||(i[t]={}),i=i[t]}i[s[s.length-1]]=a}let idCounter=0;function nextId(){return`fsb_${++idCounter}_${Date.now().toString(36)}`}export class FormSurfaceBuilder{deps;constructor(e){this.deps=e}build(e){const{schema:t,data:a,entityId:s,onSuccess:i}=e,n=e.surfaceId??`${t.domain}-${t.operation}-form`,o=e.presentation??"panel",d=`${t.domain}_${t.operation}_submit`,r=t.fields.map(e=>this.schemaFieldToA2UI(e,a)),c=[{id:nextId(),type:"Form",properties:{title:t.title,...t.description?{description:t.description}:{},fields:r,submitLabel:t.submitLabel,submitActionId:d}}],p=this.deps.canvasState.has(n),l=async e=>{if((e.userAction?.name??"")!==d)return;const a=e.userAction?.context??{};await this.handleSubmit(t,a,n,c,s,i)},u=this.buildEnvelope(n,{surfaceUpdate:{surfaceId:n,components:c}});if(p)this.deps.canvasState.update(n,c),this.deps.canvasState.bindActionHandler(n,l),this.deps.sendCanvasUpdate("_active",u);else{const e=this.buildEnvelope(n,{beginRendering:{surfaceId:n,catalogId:BEEOS_EXTENDED_CATALOG}},o);this.deps.canvasState.create(n,o,c,{origin:{kind:"system",handlerId:`${t.domain}-form`},actionHandler:l}),this.deps.sendCanvasUpdate("_active",u),this.deps.sendCanvasUpdate("_active",e)}this.deps.logger.debug?.(`[form-builder] ${p?"Updated":"Created"} form surface=${n} domain=${t.domain} op=${t.operation}`)}schemaFieldToA2UI(e,t){const a={name:e.name,label:e.label,inputType:e.inputType};e.placeholder&&(a.placeholder=e.placeholder),e.required&&(a.required=!0),e.options&&(a.options=e.options),e.validation&&(a.validation=e.validation);let s=e.defaultValue;return void 0===s&&t&&e.apiPath&&(s=getByPath(t,e.apiPath)),void 0!==s&&(a.defaultValue=s),a}async handleSubmit(e,t,a,s,i,n){try{const s=e.transformPayload?e.transformPayload(t):this.autoTransform(e.fields,t),o="update"===e.operation&&i?{id:i,patch:s}:s;await this.deps.gatewayRequest(e.targetRpc,o),this.deps.logger.info?.(`[form-builder] Submit success: ${e.targetRpc} (${e.domain}:${e.operation})`);const d={id:nextId(),type:"Alert",properties:{variant:"success",title:"Done",message:`${e.title} saved successfully.`,dismissible:!0}};this.deps.sendCanvasUpdate("_active",this.buildEnvelope(a,{surfaceUpdate:{surfaceId:a,components:[d]}})),this.deps.canvasState.update(a,[d]),setTimeout(()=>{this.deps.sendCanvasUpdate("_active",this.buildEnvelope(a,{deleteSurface:{surfaceId:a}})),this.deps.canvasState.delete(a)},3e3),n&&await n()}catch(t){const i=t instanceof Error?t.message:String(t);this.deps.logger.warn?.(`[form-builder] Submit failed: ${e.targetRpc} — ${i}`);const n=[...s,{id:nextId(),type:"Alert",properties:{variant:"error",title:"Failed",message:i,dismissible:!0}}];this.deps.sendCanvasUpdate("_active",this.buildEnvelope(a,{surfaceUpdate:{surfaceId:a,components:n}})),this.deps.canvasState.update(a,n)}}autoTransform(e,t){const a={};for(const s of e){const e=t[s.name];if(void 0===e||""===e)continue;setByPath(a,s.apiPath??s.name,e)}return a}buildEnvelope(e,t,a){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,...a?{presentation:a}:{},message:t}}}
@@ -1 +0,0 @@
1
- import{resolveColumnValue,resolveActionLabel}from"./resource-descriptor.js";const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1";let idCounter=0;function nextId(){return`rsr_${++idCounter}_${Date.now().toString(36)}`}export class ResourceSurfaceRenderer{deps;constructor(e){this.deps=e}async render(e,t="panel"){try{const n=await this.deps.gatewayRequest(e.listRpc,e.listParams??{}),s=e.extractItems(n);this.buildAndSendSurface(e,s,t)}catch(t){this.deps.logger.warn?.(`[resource-renderer] Failed to render ${e.domain}: ${t instanceof Error?t.message:t}`)}}buildAndSendSurface(e,t,n){const s=this.surfaceId(e,n),r=this.buildComponents(e,t),i=this.deps.canvasState.has(s),o=this.envelope(s,{surfaceUpdate:{surfaceId:s,components:r}}),a=t=>this.handleAction(e,t,n);if(i)this.deps.canvasState.update(s,r),this.deps.canvasState.bindActionHandler(s,a),this.deps.sendCanvasUpdate("_active",o);else{const t=this.envelope(s,{beginRendering:{surfaceId:s,catalogId:BEEOS_EXTENDED_CATALOG}},n);this.deps.canvasState.create(s,n,r,{origin:{kind:"system",handlerId:`${e.domain}-resource`},actionHandler:a}),this.deps.sendCanvasUpdate("_active",o),this.deps.sendCanvasUpdate("_active",t)}this.deps.logger.debug?.(`[resource-renderer] ${i?"Updated":"Created"} surface=${s} domain=${e.domain} items=${t.length}`)}buildComponents(e,t){const n=[],s=nextId();if(n.push({id:s,type:"Text",properties:{text:e.title,variant:"heading"}}),0===t.length){const t=nextId();n.push({id:t,type:"Callout",properties:{variant:"note",title:e.emptyTitle??"No items",content:e.emptyContent??""}});const r=nextId();return n.push({id:r,type:"Card",properties:{},children:[s,t]}),n}const r=e.columns.map(e=>({key:e.key,label:e.label})),i=t.map(t=>({cells:e.columns.map(e=>resolveColumnValue(e,t))})),o=nextId();n.push({id:o,type:"Table",properties:{title:"",columns:r,rows:i}});const a=[];for(const s of t)for(const t of e.itemActions){const r=nextId(),i=resolveActionLabel(t.label,s),o=`${e.domain}_${t.id}`;let d;d="rpc"===t.type?t.buildParams(s):{__itemId:e.itemId(s),...t.buildFormData?t.buildFormData(s):{}},n.push({id:r,type:"Button",properties:{label:i,actionId:o,context:d}}),a.push(r)}const d=nextId();n.push({id:d,type:"Grid",properties:{columns:Math.min(e.itemActions.length,4),gap:"sm"},children:a});const c=[];for(const t of e.globalActions){const s=nextId(),r=`${e.domain}_${t.id}`;n.push({id:s,type:"Button",properties:{label:t.label,actionId:r}}),c.push(s)}const p=[s,o,d,...c],l=nextId();return n.push({id:l,type:"Card",properties:{},children:p}),n}async handleAction(e,t,n){const s=t.userAction?.name??"",r=t.userAction?.context??{},i=`${e.domain}_`;if(!s.startsWith(i))return void this.deps.logger.debug?.(`[resource-renderer] Ignoring action ${s} (not in domain ${e.domain})`);const o=s.slice(i.length),a=e.globalActions.find(e=>e.id===o);if(a){if("refresh"===a.type)return void await this.render(e,n);if("form"===a.type)return void this.openForm(a.formDomain,a.formOperation,void 0,void 0,e,n)}const d=e.itemActions.find(e=>e.id===o);if(d){try{if("rpc"===d.type)await this.deps.gatewayRequest(d.rpc,r);else if("form"===d.type){const t=r.__itemId??void 0,s={...r};return delete s.__itemId,void this.openForm(d.formDomain,d.formOperation,Object.keys(s).length>0?s:void 0,t,e,n)}}catch(e){this.deps.logger.warn?.(`[resource-renderer] Action ${s} failed: ${e instanceof Error?e.message:e}`)}await this.render(e,n)}else this.deps.logger.debug?.(`[resource-renderer] Unknown action: ${s}`)}openForm(e,t,n,s,r,i){const o=this.deps.schemaRegistry.get(e,t);o?this.deps.formBuilder.build({schema:o,data:n,entityId:s,onSuccess:()=>this.render(r,i)}):this.deps.logger.warn?.(`[resource-renderer] Form schema not found: ${e}:${t}`)}surfaceId(e,t){return"inline"===t?`${e.domain}-resource-inline`:`${e.domain}-resource`}envelope(e,t,n){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,...n?{presentation:n}:{},message:t}}}
@@ -1 +0,0 @@
1
- import{TERMINAL_UPDATE_METHOD,parseTerminalOpenParams,parseTerminalInputParams,parseTerminalResizeParams,parseTerminalCloseParams,parseTerminalWaitParams}from"./terminal-rpc.js";import{Errors}from"./errors/index.js";export class TerminalBridgeHandler{terminalMgr;transport;config;logger;writeObs;interactiveTerminals=new Map;HEARTBEAT_TIMEOUT_MS=6e4;constructor(e){this.terminalMgr=e.terminalMgr,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs}setupListeners(){this.terminalMgr.addListeners({onOutput:e=>{this.interactiveTerminals.has(e.terminalId)&&this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"stdout",dataBase64:e.data.toString("base64"),seq:e.seq})},onExit:e=>{const r=this.interactiveTerminals.get(e.terminalId);r&&(clearTimeout(r.heartbeatTimer),this.interactiveTerminals.delete(e.terminalId),this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"exit",code:e.code,signal:e.signal,...e.reason?{reason:e.reason}:{}}))},onStateChange:e=>{this.interactiveTerminals.has(e.terminalId)&&this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"state",from:e.from,to:e.to})}})}async handleRequest(e){switch(e.method){case"terminal/open":await this.handleTerminalOpen(e);break;case"terminal/input":this.handleTerminalInput(e);break;case"terminal/resize":this.handleTerminalResize(e);break;case"terminal/close":await this.handleTerminalClose(e);break;case"terminal/create":await this.handleTerminalCreate(e);break;case"terminal/wait":await this.handleTerminalWait(e);break;case"terminal/release":await this.handleTerminalRelease(e);break;case"terminal/kill":await this.handleTerminalKill(e);break;default:this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}handleNotification(e,r){switch(e){case"terminal/input":this.handleTerminalInputNotification(r);break;case"terminal/resize":this.handleTerminalResizeNotification(r);break;default:this.logger.debug?.(`[terminal-handler] Unknown notification: ${e}`)}}handleTerminalInputNotification(e){if(!this.config.bridge.shell.enabled)return;const r=parseTerminalInputParams(e);if(!r.ok)return;let a;if(r.value.dataBase64)try{a=Buffer.from(r.value.dataBase64,"base64")}catch{return}else a=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,a),this.refreshTerminalHeartbeat(r.value.terminalId)}catch{}}handleTerminalResizeNotification(e){if(!this.config.bridge.shell.enabled)return;const r=parseTerminalResizeParams(e);if(r.ok)try{this.terminalMgr.resizeSession(r.value.terminalId,r.value.cols,r.value.rows),this.refreshTerminalHeartbeat(r.value.terminalId)}catch{}}async handleTerminalCreate(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalOpenParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);this.terminalMgr.sessionCount>=this.terminalMgr.maxConcurrentLimit&&this.logger.warn?.("[terminal-handler] Terminal quota exceeded, evicting oldest");const a=this.terminalMgr.create(r.value.command);this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.created",severity:"info",payload:{terminalId:a.id,command:a.command,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:a.id,command:a.command})}async handleTerminalWait(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalWaitParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.terminalMgr.get(r.value.terminalId);a?this.transport.sendResult(e.id,{terminalId:r.value.terminalId,exitCode:a.exitCode,output:a.output.join(""),state:a.state}):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalRelease(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);this.terminalMgr.get(r.value.terminalId)?(this.terminalMgr.release(r.value.terminalId),this.transport.sendResult(e.id,{})):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalKill(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.terminalMgr.get(r.value.terminalId);a?"closed"!==a.state?(this.terminalMgr.kill(r.value.terminalId,r.value.signal),this.transport.sendResult(e.id,{})):this.transport.sendAcpError(e.id,Errors.TerminalClosed):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalOpen(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalOpenParams(e.params);if(r.ok)try{const a=this.terminalMgr.openSession({sessionId:r.value.sessionId,command:r.value.command,cols:r.value.cols,rows:r.value.rows,cwd:r.value.cwd}),t=setTimeout(()=>{this.interactiveTerminals.delete(a.terminalId),this.terminalMgr.closeSession(a.terminalId,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS);"object"==typeof t&&"unref"in t&&t.unref(),this.interactiveTerminals.set(a.terminalId,{heartbeatTimer:t}),this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.opened",severity:"info",payload:{terminalId:a.terminalId,shell:a.shell,cwd:a.cwd,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:a.terminalId,command:a.shell,shell:a.shell,cwd:a.cwd,cols:a.cols,rows:a.rows,state:a.state})}catch(r){const a=r instanceof Error?r.message:String(r);a.includes("quota")?this.transport.sendAcpError(e.id,Errors.TerminalQuotaExceeded,{message:a}):this.transport.sendAcpError(e.id,Errors.GeneralError,{message:a})}else this.sendTerminalParseError(e.id,r)}handleTerminalInput(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalInputParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);let a;if(r.value.dataBase64)try{a=Buffer.from(r.value.dataBase64,"base64")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"invalid dataBase64"})}else a=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,a),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const a=r instanceof Error?r.message:String(r);this.transport.sendAcpError(e.id,Errors.TerminalNotFound,{message:a})}}handleTerminalResize(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalResizeParams(e.params);if(r.ok)try{this.terminalMgr.resizeSession(r.value.terminalId,r.value.cols,r.value.rows),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const a=r instanceof Error?r.message:String(r);this.transport.sendAcpError(e.id,Errors.TerminalNotFound,{message:a})}else this.sendTerminalParseError(e.id,r)}async handleTerminalClose(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.interactiveTerminals.get(r.value.terminalId);a&&(clearTimeout(a.heartbeatTimer),this.interactiveTerminals.delete(r.value.terminalId)),this.terminalMgr.closeSession(r.value.terminalId),this.transport.sendResult(e.id,{})}refreshTerminalHeartbeat(e){const r=this.interactiveTerminals.get(e);r&&(clearTimeout(r.heartbeatTimer),r.heartbeatTimer=setTimeout(()=>{this.logger.warn?.(`[terminal-handler] Terminal heartbeat timeout terminalId=${e}`),this.interactiveTerminals.delete(e),this.terminalMgr.closeSession(e,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS),"object"==typeof r.heartbeatTimer&&"unref"in r.heartbeatTimer&&r.heartbeatTimer.unref())}sendTerminalParseError(e,r){this.transport.sendAcpError(e,Errors.InvalidParams,{message:r.error.message})}destroy(){for(const e of this.interactiveTerminals.values())clearTimeout(e.heartbeatTimer);this.interactiveTerminals.clear()}}
@@ -1 +0,0 @@
1
- import{Errors}from"./errors/index.js";export const TERMINAL_METHODS={open:"terminal/open",input:"terminal/input",resize:"terminal/resize",close:"terminal/close",create:"terminal/create",wait:"terminal/wait",release:"terminal/release",kill:"terminal/kill"};export const TERMINAL_UPDATE_METHOD="terminal/update";export const TERMINAL_ERROR_CODES={shellDisabled:-32010,terminalNotFound:-32011,terminalClosed:-32012,terminalQuotaExceeded:-32013,terminalTimeout:-32014};export const TERMINAL_INVALID_PARAMS={code:-32602,message:"invalid params"};function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function readString(e,r){const t=e[r];if("string"==typeof t){const e=t.trim();return e.length>0?e:void 0}}function readPositiveInt(e,r){const t=e[r];if("number"==typeof t&&Number.isInteger(t)&&t>0)return t}function invalid(e,r){return{ok:!1,error:{code:Errors.InvalidParams.rpcCode,message:Errors.InvalidParams.message,data:{field:e,reason:r}}}}function parseBase(e){return isRecord(e)?{ok:!0,value:e}:invalid("params","must be an object")}export function parseTerminalOpenParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readPositiveInt(t,"cols")??80,a=readPositiveInt(t,"rows")??24,i=readString(t,"sessionId"),s=readString(t,"command"),o=readString(t,"cwd");return{ok:!0,value:{sessionId:i,command:s,cols:n,rows:a,...o?{cwd:o}:{}}}}export function parseTerminalInputParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(t,"data"),i=readString(t,"dataBase64");return a||i?{ok:!0,value:{terminalId:n,data:a,dataBase64:i}}:invalid("data","data or dataBase64 must be provided")}export function parseTerminalResizeParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(t,"cols");if(!a)return invalid("cols","must be a positive integer");const i=readPositiveInt(t,"rows");return i?{ok:!0,value:{terminalId:n,cols:a,rows:i}}:invalid("rows","must be a positive integer")}export function parseTerminalCloseParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(t,"signal");return{ok:!0,value:{terminalId:n,...a?{signal:a}:{}}}}export function parseTerminalWaitParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(t,"timeoutMs");return{ok:!0,value:{terminalId:n,...a?{timeoutMs:a}:{}}}}export function isTerminalRpcMethod(e){return Object.values(TERMINAL_METHODS).includes(e)}
@@ -1 +0,0 @@
1
- import{WebSocketServer,WebSocket}from"ws";const HEARTBEAT_TIMEOUT_MS=6e4;export function createTerminalWebSocketService(e){const{config:t,terminalMgr:n,logger:o,writeObs:r}=e,i=r??(()=>{});let a=null;const c=new Set;function s(e){return e.replace(/^https?:\/\//,"").replace(/:\d+$/,"").replace(/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i,"localhost")}function l(e,r){const a=function(e){const t=e.headers["x-forwarded-for"];return("string"==typeof t?t.split(",")[0]?.trim():void 0)||e.socket?.remoteAddress||"unknown"}(r),l=r.headers.origin;if(!function(e){if(!t.allowedOrigins||0===t.allowedOrigins.length)return!0;if(!e)return!1;const n=s(e);return t.allowedOrigins.some(e=>s(e)===n)}(l))return i({component:"terminal-ws",domain:"auth",name:"origin_rejected",severity:"warn",payload:{origin:l,clientIdentity:a}}),void e.close(1008,"Origin not allowed");const d=function(e){try{return new URL(e.url??"/","ws://localhost").searchParams.get("token")??void 0}catch{return}}(r);if(d!==t.token)return i({component:"terminal-ws",domain:"auth",name:"token_rejected",severity:"warn",payload:{clientIdentity:a}}),void e.close(1008,"Unauthorized");const f=n.create(),m={ws:e,terminalId:f.id,clientIdentity:a};c.add(m),i({component:"terminal-ws",domain:"lifecycle",name:"client.connected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}});const u=setInterval(()=>{const t=n.get(f.id);if(t){if(t.output.length>0){const n=t.output.splice(0).join("");n&&e.readyState===WebSocket.OPEN&&e.send(n)}"closed"===t.state&&(clearInterval(u),e.readyState===WebSocket.OPEN&&e.close(1e3,`exit:${t.exitCode??0}`))}else clearInterval(u)},50);let p=Date.now();const w=setInterval(()=>{Date.now()-p>6e4&&(o?.warn?.(`[terminal-ws] heartbeat timeout terminalId=${f.id}`),e.close(1001,"Heartbeat timeout"))},3e4);"object"==typeof w&&"unref"in w&&w.unref(),e.on("message",(t,o)=>{p=Date.now();const r=Buffer.isBuffer(t)?t.toString("utf-8"):"string"==typeof t?t:Buffer.from(t).toString("utf-8");if(o)n.write(f.id,r);else try{const t=JSON.parse(r),o=t.action??t.type;if("heartbeat"===o)p=Date.now(),e.send(JSON.stringify({type:"heartbeat_ack"}));else if("resize"===o){const e="number"==typeof t.cols?t.cols:80,o="number"==typeof t.rows?t.rows:24;n.resize(f.id,e,o)}else"input"===o&&"string"==typeof t.data&&n.write(f.id,t.data)}catch{n.write(f.id,r)}}),e.on("close",()=>{clearInterval(u),clearInterval(w),c.delete(m),n.release(f.id),i({component:"terminal-ws",domain:"lifecycle",name:"client.disconnected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}})})}return{start(){a=new WebSocketServer({port:t.port}),a.on("connection",l),o?.info?.(`[terminal-ws] Listening on ws://0.0.0.0:${t.port}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.started",severity:"info",payload:{port:t.port}})},stop(){for(const e of c)try{e.ws.close(1001,"Server shutting down")}catch{}c.clear(),a?.close(),a=null}}}
@@ -1 +0,0 @@
1
- import{constants}from"node:fs";import{access,readFile,stat}from"node:fs/promises";import{basename,extname}from"node:path";import{agentFetch}from"./agent-auth.js";export const BEEOS_UPLOAD_TOOL_NAME="beeos_upload_file";export const BEEOS_UPLOAD_TOOL_DESCRIPTION="Upload 1-5 local files from this machine to the user via the BeeOS platform. If the call succeeds, the files are already delivered to the user — do not use any other tool to send them again. Any file type is accepted: text, images, documents, PDFs, archives, binaries.";export const BEEOS_UPLOAD_TOOL_SCHEMA={title:"BeeOS file upload",type:"object",description:BEEOS_UPLOAD_TOOL_DESCRIPTION,additionalProperties:!1,required:["paths"],properties:{paths:{type:"array",minItems:1,maxItems:5,description:"Local filesystem paths (1-5). Absolute or workspace-relative.",items:{type:"string",minLength:1,description:"Single local filesystem path"}}}};const MAX_PATHS=5,REQUEST_TIMEOUT_MS=12e4,MAX_CONCURRENCY=3;async function runWithConcurrency(e,t){const r=new Array(e.length);let a=0;async function n(){for(;a<e.length;){const t=a++;r[t]=await e[t]()}}const o=[];for(let r=0;r<Math.min(t,e.length);r++)o.push(n());return await Promise.all(o),r}async function validateFiles(e){return Promise.all(e.map(async e=>{try{await access(e,constants.R_OK);return(await stat(e)).isFile()?{path:e,ok:!0}:{path:e,ok:!1,error:"not a regular file"}}catch(t){const r=t.code;let a;return a="ENOENT"===r?"file not found":"ENOTDIR"===r?"path component is not a directory":"EACCES"===r?"permission denied":t instanceof Error?t.message:String(t),{path:e,ok:!1,error:a}}}))}export async function executeUpload(e,t,r){const a=parseParams(e);if(!a.ok)return errorResult(a.error);if(!t.platformUrl)return errorResult("platformUrl not configured — file upload requires platform API access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const n=`${t.platformUrl}/api/v1/agent/files`,o=a.paths,i=await validateFiles(o),s=i.filter(e=>!e.ok);if(s.length===o.length)return errorResult(`All files invalid: ${s.map(e=>`${e.path}: ${e.error}`).join("; ")}`);const l=i.filter(e=>e.ok).map(e=>e.path),c=s.map(e=>`${e.path}: ${e.error}`),p=l.map(e=>async()=>{try{return await uploadSingleFile(e,n,r)}catch(t){return c.push(`${e}: ${t instanceof Error?t.message:String(t)}`),null}}),u=(await runWithConcurrency(p,3)).filter(e=>null!==e);if(0===u.length)return errorResult(`All uploads failed: ${c.join("; ")}`);const f=u.map(e=>({uri:`beeos-file://${e.fileId}`,name:e.fileName,mimeType:e.mimeType,fileId:e.fileId})),m={ok:!0,files:f};return{...m,content:f.map(e=>({type:"resource_link",uri:e.uri,name:e.name,mimeType:e.mimeType})),output:m,result:m,details:m,isError:!1}}async function uploadSingleFile(e,t,r){await access(e,constants.R_OK);if(!(await stat(e)).isFile())throw new Error("not a regular file");const a=basename(e),n=inferMimeType(a),o=await readFile(e),i=t+"/presign";try{return await uploadViaPresign(i,t,r,a,n,o)}catch{return await uploadViaMultipart(t,r,a,n,o)}}function bufferToArrayBuffer(e){const t=new ArrayBuffer(e.byteLength),r=new Uint8Array(t);for(let t=0;t<e.byteLength;t++)r[t]=e[t];return t}async function uploadViaPresign(e,t,r,a,n,o){const i=new AbortController,s=setTimeout(()=>i.abort(),12e4);"object"==typeof s&&"unref"in s&&s.unref();try{const s=await agentFetch(e,r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fileName:a,mimeType:n}),signal:i.signal});if(!s.ok){const e=await s.text().catch(()=>s.statusText);throw new Error(`presign ${s.status}: ${e}`)}const l=await s.json(),c=new Blob([bufferToArrayBuffer(o)],{type:n}),p=await fetch(l.uploadUrl,{method:"PUT",headers:{"Content-Type":n},body:c,signal:i.signal});if(!p.ok)throw new Error(`S3 PUT failed: ${p.status}`);const u=t+"/confirm",f=await agentFetch(u,r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fileId:l.fileId}),signal:i.signal});if(f.ok){const e=await f.json();return{fileId:e.fileId,downloadUrl:e.downloadUrl,fileName:e.fileName||a,mimeType:e.mimeType||n,size:e.size||o.byteLength}}return{fileId:l.fileId,downloadUrl:l.uploadUrl.split("?")[0]??l.uploadUrl,fileName:a,mimeType:n,size:o.byteLength}}finally{clearTimeout(s)}}async function uploadViaMultipart(e,t,r,a,n){const o=new FormData;o.append("file",new Blob([bufferToArrayBuffer(n)],{type:a}),r);const i=new AbortController,s=setTimeout(()=>i.abort(),12e4);let l;"object"==typeof s&&"unref"in s&&s.unref();try{l=await agentFetch(e,t,{method:"POST",body:o,signal:i.signal})}catch(e){if(e instanceof Error&&"AbortError"===e.name)throw new Error("upload timed out after 120000ms");throw e}finally{clearTimeout(s)}if(!l.ok){const e=await l.text().catch(()=>l.statusText);throw new Error(`HTTP ${l.status}: ${e}`)}return await l.json()}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e;if(!Array.isArray(t.paths))return{ok:!1,error:"paths must be an array"};if(t.paths.length<1)return{ok:!1,error:"paths must contain at least 1 item"};if(t.paths.length>5)return{ok:!1,error:"paths must contain no more than 5 items"};const r=[];for(let e=0;e<t.paths.length;e++){const a=t.paths[e];if("string"!=typeof a||""===a.trim())return{ok:!1,error:`paths[${e}] must be a non-empty string`};r.push(a.trim())}return{ok:!0,paths:r}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}function inferMimeType(e){switch(extname(e).toLowerCase()){case".txt":return"text/plain";case".md":return"text/markdown";case".json":return"application/json";case".html":return"text/html";case".css":return"text/css";case".js":case".mjs":case".cjs":return"text/javascript";case".ts":return"text/x.typescript";case".tsx":case".jsx":return"text/jsx";case".xml":return"application/xml";case".yaml":case".yml":return"application/yaml";case".pdf":return"application/pdf";case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".gif":return"image/gif";case".webp":return"image/webp";case".svg":return"image/svg+xml";case".csv":return"text/csv";case".zip":return"application/zip";case".tar":return"application/x-tar";case".gz":case".tgz":return"application/gzip";default:return"application/octet-stream"}}