beeos-claw 0.1.10 → 0.1.12

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.
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas-tools.js";import{MessageQueue}from"../utils/mq.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{appendHistoryEntry}from"./local-session-history.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const a=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return a&&!a.includes("/")&&SLASH_COMMAND_NAME_RE.test(a)?a.toLowerCase():void 0}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;handleTerminalRequest=null;handleTerminalNotification=null;handleSlashCommand=null;onPromptStart=null;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":this.handleSessionDelete(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/action":case"canvas/toggle":case"canvas/clear":case"canvas/delete":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,a=this.state.resolveSessionFromParams(t);if(!a){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}a.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`session:${randomUUID()}`,s=`agent:main:${randomUUID()}`,a="string"==typeof e.params?.cwd?e.params.cwd:".",i={sessionId:t,gatewaySessionKey:s,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:a};this.state.sessions.set(t,i),this.state.sessionsByGatewayKey.set(s,i),this.transport.sendResult(e.id,{sessionId:t,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});let s=this.state.sessions.get(t);if(!s){const e=t.startsWith("agent:")?t:`agent:main:${t}`;s={sessionId:t,gatewaySessionKey:e,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(t,s),this.state.sessionsByGatewayKey.set(e,s)}this.state.lastActiveSessionId=t,s.chatReplayDedupKeys.clear();try{const e=this.getGateway(),a=await loadHistory(e,s.gatewaySessionKey,50),i=groupIntoTurns(a),o=i.filter(e=>e.toolCalls?.length).length,n=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${t} messages=${a.length} turns=${i.length} turnsWithToolCalls=${o} totalToolCalls=${n}`);for(const e of i){const a=e.toolCalls?.length?`:tc=${e.toolCalls.map(e=>e.id).join(",")}`:"",i=`${e.role}:${e.content.slice(0,64)}${a}`;s.chatReplayDedupKeys.has(i)||(s.chatReplayDedupKeys.add(i),this.transport.sendSessionUpdate(t,{sessionUpdate:"history_turn",role:e.role,content:e.content,reasoning:e.reasoning,toolCalls:e.toolCalls}))}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:t});void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed&&this.replayCurrentAssistantStream(s).catch(()=>{}),this.pushSkillsList().catch(()=>{})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const t=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,s=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let a=0;for(;Date.now()<s&&(e.assistantStreamOwnerRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(a),new Promise(e=>setTimeout(()=>e(null),t))]);if(!s||0===s.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t),a++}a>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:a}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,a=s.sessionKey??s.key??s.id??s.name;if(!a||"string"!=typeof a)continue;if(isBackgroundSessionKey(a))continue;const i=t.find(e=>e.sessionId===a||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===a),o=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0;if(i){if(!i.title||"New chat"===i.title){const e=o||a;i.title=e;const t=this.state.sessions.get(i.sessionId);t&&!t.title&&(t.title=e)}const e=s.updatedAt;e&&(i.updatedAt=e)}else t.push({sessionId:a,title:o||a,cwd:s.cwd,updatedAt:s.updatedAt})}}}catch{}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const a=s.slice(0,64),i=this.state.sessions.get(t);i&&(i.title=a);const o=i?.gatewaySessionKey??(t.startsWith("agent:")?t:`agent:main:${t}`),n=this.getGateway();n?.isConnected&&n.sessionsPatch(o,{label:a}).catch(()=>{}),this.transport.sendResult(e.id,{sessionId:t,title:a})}handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);s?(void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId),this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(s.gatewaySessionKey),this.transport.sendResult(e.id,{deleted:!0})):this.transport.sendAcpError(e.id,Errors.SessionNotFound)}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(a.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s)try{await i.sessionsPatch(a.gatewaySessionKey,{model:s})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:s??"default"})}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});let a=this.state.sessions.get(t);if(!a){const e=t.startsWith("agent:")?t:`agent:main:${t}`;a={sessionId:t,gatewaySessionKey:e,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(t,a),this.state.sessionsByGatewayKey.set(e,a)}this.state.lastActiveSessionId=t,this.onPromptStart?.();const i=this.getGateway();if(!i?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let o=extractPromptBlocks(sanitizePromptPayload(s));if(!a.title){const e=o.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(a.title=e.text.slice(0,64).replace(/\n/g," ").trim(),i?.isConnected&&a.title&&i.sessionsPatch(a.gatewaySessionKey,{label:a.title}).catch(()=>{}))}const n=buildResolutionPlan(o);if(n.refs.length>0)try{const e=await resolveResources(n,this.writeObs);o=applyResolutions(o,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const r=toGatewayPrompt(o);if("error"in r)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:r.error});const{message:d,attachments:l}=r,c=resolveStandaloneSlashCommandName(o,d)??resolveSlashCmd(o,d),m="string"==typeof c;if(c&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(c)}catch(e){return s.logger.warn?.(`[bridge-core] /${c} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(a.sessionId,o),this.transport.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${c}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const p=m?"chat.send":"agent";let h=m?d:withUserMessagePrefix(d);if(!this.config.canvasEnabled||m||a.canvasPromptInjected||!1===a.canvasActive||(h=`${CANVAS_TERM_MAPPING}\n\n${h}`,a.canvasPromptInjected=!0,this.logger.debug?.("[bridge-core] Canvas term mapping injected into first message")),this.config.canvasEnabled&&!m&&!1!==a.canvasActive){const e=this.getCanvasState?.();if(e){const t=buildActiveSurfacesContext(e);t&&(h=`${t}\n\n${h}`)}const t=a.referencedSurfaceId;t&&(h=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${h}`,a.referencedSurfaceId=void 0)}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${t} command=/${c}`),a.currentRunId&&this.state.promptsByRunId.delete(a.currentRunId),a.currentRunId=void 0,a.assistantStreamOwnerRunId=void 0,a.pendingPromptId=e.id,a.promptDone=!1,a.gatewayMethod=p,appendHistoryEntry(a.gatewaySessionKey,{ts:Date.now(),role:"user",content:d}),a.assistantMq?.close(),a.assistantMq=new MessageQueue,a.chatReplayDedupKeys.clear(),a.hasAgentAssistantStream=!1,a.hasAgentThinkingStream=!1,a.hasAgentToolStartStream=!1,a.hasAgentToolResultStream=!1,a.chatAssistantTextSoFar=void 0,a.agentAssistantTextSoFar=void 0,a.lastAgentSeq=void 0,a.lastChatSeq=void 0,this.transport.resetSessionUpdateCount(t),this.emitPromptAsUserSessionUpdates(a.sessionId,o),this.transport.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(a);try{const e=l.length>0?l.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let t;t=m?await i.chatSend(a.gatewaySessionKey,h,e):await i.agentSend(a.gatewaySessionKey,h,this.config.gateway.agentId||"main",e),a.currentRunId=t.runId,a.assistantStreamOwnerRunId=t.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,a)}catch(t){this.state.clearPromptTimeout(a),void 0!==a.pendingPromptId&&(a.pendingPromptId=void 0,a.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:t instanceof Error?t.message:"failed to send prompt to gateway"}))}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,a=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,n=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...a?{mimeType:a}:{},...i?{text:i}:{},...o?{data:o}:{},...n?{fileName:n}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const a=this.state.sessions.get(s);if(!a)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&a.currentRunId&&i.chatAbort(a.gatewaySessionKey,a.currentRunId).catch(()=>{}),this.completePrompt(a,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.gatewayRequestId=void 0,e.assistantMq?.close()}completePrompt(e,t){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e))}failPrompt(e,t,s){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendError(e.pendingPromptId,t,s),this.cleanupPrompt(e))}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,-32021,s)}}pushToolResultContent(e,t,s,a){if(0===s.length)return;const i=(a?this.state.promptsByRunId.get(a):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${a})`);const o=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const a=await t.request(s,e.params??{});this.transport.sendResult(e.id,a??{})}catch(t){const a=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${a}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:a})}}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
1
+ import{randomUUID}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas-tools.js";import{MessageQueue}from"../utils/mq.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{appendHistoryEntry}from"./local-session-history.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const a=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return a&&!a.includes("/")&&SLASH_COMMAND_NAME_RE.test(a)?a.toLowerCase():void 0}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;handleTerminalRequest=null;handleTerminalNotification=null;handleSlashCommand=null;onPromptStart=null;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":this.handleSessionDelete(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/action":case"canvas/toggle":case"canvas/clear":case"canvas/delete":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,a=this.state.resolveSessionFromParams(t);if(!a){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}a.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`session:${randomUUID()}`,s=`agent:main:${randomUUID()}`,a="string"==typeof e.params?.cwd?e.params.cwd:".",i={sessionId:t,gatewaySessionKey:s,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:a};this.state.sessions.set(t,i),this.state.sessionsByGatewayKey.set(s,i),this.transport.sendResult(e.id,{sessionId:t,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});let s=this.state.sessions.get(t);if(!s){const e=t.startsWith("agent:"),a=e?`session:${randomUUID()}`:t,i=e?t:`agent:main:${t}`;s={sessionId:a,gatewaySessionKey:i,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(a,s),this.state.sessionsByGatewayKey.set(i,s)}const a=s.sessionId;this.state.lastActiveSessionId=a,s.chatReplayDedupKeys.clear();try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,50),i=groupIntoTurns(t),o=i.filter(e=>e.toolCalls?.length).length,n=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${a} messages=${t.length} turns=${i.length} turnsWithToolCalls=${o} totalToolCalls=${n}`);for(const e of i){const t=e.toolCalls?.length?`:tc=${e.toolCalls.map(e=>e.id).join(",")}`:"",i=`${e.role}:${e.content.slice(0,64)}${t}`;s.chatReplayDedupKeys.has(i)||(s.chatReplayDedupKeys.add(i),this.transport.sendSessionUpdate(a,{sessionUpdate:"history_turn",role:e.role,content:e.content,reasoning:e.reasoning,toolCalls:e.toolCalls}))}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:a});void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed&&this.replayCurrentAssistantStream(s).catch(()=>{}),this.pushSkillsList().catch(()=>{})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const t=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,s=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let a=0;for(;Date.now()<s&&(e.assistantStreamOwnerRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(a),new Promise(e=>setTimeout(()=>e(null),t))]);if(!s||0===s.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t),a++}a>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:a}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,a=s.sessionKey??s.key??s.id??s.name;if(!a||"string"!=typeof a)continue;if(isBackgroundSessionKey(a))continue;const i=t.find(e=>e.sessionId===a||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===a),o=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0;if(i){if(!i.title||"New chat"===i.title){const e=o||i.sessionId;i.title=e;const t=this.state.sessions.get(i.sessionId);t&&!t.title&&(t.title=e)}const e=s.updatedAt;e&&(i.updatedAt=e)}else{const e=`session:${randomUUID()}`,i={sessionId:e,gatewaySessionKey:a,createdAt:Date.now(),chatReplayDedupKeys:new Set,title:o||"New chat",cwd:s.cwd};this.state.sessions.set(e,i),this.state.sessionsByGatewayKey.set(a,i),t.push({sessionId:e,title:o||"New chat",cwd:s.cwd,updatedAt:s.updatedAt})}}}}catch{}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const a=s.slice(0,64),i=this.state.sessions.get(t);i&&(i.title=a);const o=i?.gatewaySessionKey??`agent:main:${t}`,n=this.getGateway();n?.isConnected&&n.sessionsPatch(o,{label:a}).catch(()=>{}),this.transport.sendResult(e.id,{sessionId:t,title:a})}handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);s?(void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId),this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(s.gatewaySessionKey),this.transport.sendResult(e.id,{deleted:!0})):this.transport.sendAcpError(e.id,Errors.SessionNotFound)}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(a.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s)try{await i.sessionsPatch(a.gatewaySessionKey,{model:s})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:s??"default"})}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});let a=this.state.sessions.get(t);if(!a){const e=t.startsWith("agent:"),s=e?`session:${randomUUID()}`:t,i=e?t:`agent:main:${t}`;a={sessionId:s,gatewaySessionKey:i,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(s,a),this.state.sessionsByGatewayKey.set(i,a)}const i=a.sessionId;this.state.lastActiveSessionId=i,this.onPromptStart?.();const o=this.getGateway();if(!o?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let n=extractPromptBlocks(sanitizePromptPayload(s));if(!a.title){const e=n.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(a.title=e.text.slice(0,64).replace(/\n/g," ").trim(),o?.isConnected&&a.title&&o.sessionsPatch(a.gatewaySessionKey,{label:a.title}).catch(()=>{}))}const r=buildResolutionPlan(n);if(r.refs.length>0)try{const e=await resolveResources(r,this.writeObs);n=applyResolutions(n,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const d=toGatewayPrompt(n);if("error"in d)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:d.error});const{message:l,attachments:c}=d,m=resolveStandaloneSlashCommandName(n,l)??resolveSlashCmd(n,l),p="string"==typeof m;if(m&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(m)}catch(e){return t.logger.warn?.(`[bridge-core] /${m} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(a.sessionId,n),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${m}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const h=p?"chat.send":"agent";let u=p?l:withUserMessagePrefix(l);if(!this.config.canvasEnabled||p||a.canvasPromptInjected||!1===a.canvasActive||(u=`${CANVAS_TERM_MAPPING}\n\n${u}`,a.canvasPromptInjected=!0,this.logger.debug?.("[bridge-core] Canvas term mapping injected into first message")),this.config.canvasEnabled&&!p&&!1!==a.canvasActive){const e=this.getCanvasState?.();if(e){const t=buildActiveSurfacesContext(e);t&&(u=`${t}\n\n${u}`)}const t=a.referencedSurfaceId;t&&(u=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${u}`,a.referencedSurfaceId=void 0)}p&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${i} command=/${m}`),a.currentRunId&&this.state.promptsByRunId.delete(a.currentRunId),a.currentRunId=void 0,a.assistantStreamOwnerRunId=void 0,a.pendingPromptId=e.id,a.promptDone=!1,a.gatewayMethod=h,appendHistoryEntry(a.gatewaySessionKey,{ts:Date.now(),role:"user",content:l}),a.assistantMq?.close(),a.assistantMq=new MessageQueue,a.chatReplayDedupKeys.clear(),a.hasAgentAssistantStream=!1,a.hasAgentThinkingStream=!1,a.hasAgentToolStartStream=!1,a.hasAgentToolResultStream=!1,a.chatAssistantTextSoFar=void 0,a.agentAssistantTextSoFar=void 0,a.lastAgentSeq=void 0,a.lastChatSeq=void 0,this.transport.resetSessionUpdateCount(i),this.emitPromptAsUserSessionUpdates(i,n),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(a);try{const e=c.length>0?c.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let t;t=p?await o.chatSend(a.gatewaySessionKey,u,e):await o.agentSend(a.gatewaySessionKey,u,this.config.gateway.agentId||"main",e),a.currentRunId=t.runId,a.assistantStreamOwnerRunId=t.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,a)}catch(t){this.state.clearPromptTimeout(a),void 0!==a.pendingPromptId&&(a.pendingPromptId=void 0,a.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:t instanceof Error?t.message:"failed to send prompt to gateway"}))}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,a=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,n=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...a?{mimeType:a}:{},...i?{text:i}:{},...o?{data:o}:{},...n?{fileName:n}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const a=this.state.sessions.get(s);if(!a)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&a.currentRunId&&i.chatAbort(a.gatewaySessionKey,a.currentRunId).catch(()=>{}),this.completePrompt(a,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.gatewayRequestId=void 0,e.assistantMq?.close()}completePrompt(e,t){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e))}failPrompt(e,t,s){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendError(e.pendingPromptId,t,s),this.cleanupPrompt(e))}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,s)}}pushToolResultContent(e,t,s,a){if(0===s.length)return;const i=(a?this.state.promptsByRunId.get(a):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${a})`);const o=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const a=await t.request(s,e.params??{});this.transport.sendResult(e.id,a??{})}catch(t){const a=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${a}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:a})}}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
@@ -1 +1 @@
1
- import{stripTransportMetadata}from"../message-filter.js";import{appendHistoryEntry}from"./local-session-history.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS,DEFAULT_CRON_FLUSH_TIMEOUT_MS,DEFAULT_CRON_SIGNAL_WINDOW_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"!=typeof t)return;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||void 0}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return void(this.state.isCronCandidate(t.runId,e)&&this.handleUncorrelatedCronChatEvent(t))}if("number"==typeof t.seq){if(t.seq<=(e.lastChatSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate chat event seq=${t.seq} (last=${e.lastChatSeq}) runId=${t.runId}`);e.lastChatSeq=t.seq}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role)for(const o of n.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(a){if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=`chat:thinking:${t}`;if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},r=`chat:tool_start:${t}:${JSON.stringify(a)}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const r=`chat:tool_result:${t}:${a}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=`chat:text:${o.slice(0,128)}`;if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=o,o.startsWith(r))i=o.slice(r.length);else{if(r.startsWith(o))continue;i=o}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${r.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i}};this.transport.sendSessionUpdate(s,l),e.assistantMq?.push(l)}}}}else"delta"===t.state&&this.touchStreamActivity(e);this.maybeCompletePromptFromChatState(e,t),"final"===t.state?this.completePrompt(e,t.stopReason||"end_turn"):"aborted"===t.state||"cancelled"===t.state||"cancel"===t.state?this.completePrompt(e,"cancelled"):"error"===t.state&&this.failPrompt(e,-32021,t.errorMessage||"Agent error")}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0)??this.state.findCronTargetSession();if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return this.state.isCronCandidate(t.runId,e)?void this.handleUncorrelatedCronAgentEvent(t):void 0}if("number"==typeof t.seq){if(t.seq<=(e.lastAgentSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate agent event seq=${t.seq} (last=${e.lastAgentSeq}) runId=${t.runId} stream=${t.stream}`);e.lastAgentSeq=t.seq}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${o.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}}));const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:s,toolName:o});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});continue}}appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:o??""})}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=t.data?.delta||t.data?.text;if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:n,entryType:"thought"})}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:t,toolName:o})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool",a=this.resolveToolCallId(e,{...s,...t},"start");n({sessionUpdate:"tool_call",toolCallId:a,title:o,kind:"other",status:"in_progress"}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:"",entryType:"tool_call",toolCallId:a,toolName:o})}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){this.flushCronBufferForRun(t.runId);const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"end_turn")}else if("cancelled"===n||"cancel"===n){const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"cancelled")}else if("error"===n){const n=s?.error,o=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n&&"object"==typeof n?JSON.stringify(n):void 0)??"gateway lifecycle error";this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${o} data=${JSON.stringify(s)}`),this.failPrompt(e,-32021,o)}}}async handleToolResult(t,e){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,r=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",i="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:r,rawText:i,rawResult:e.data})}catch{c=i?[{type:"content",content:{type:"text",text:i}}]:[]}if(0===c.length&&i&&(c=[{type:"content",content:{type:"text",text:i}}]),c.length>0){const e=c.filter(t=>"text"===t.content?.type&&t.content?.text).map(t=>({type:"content",content:{type:"text",text:t.content.text}})),s={sessionUpdate:"tool_call_update",toolCallId:a,title:r,kind:"other",status:"completed",...e.length>0?{content:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s)&&t.assistantMq?.push(s),appendHistoryEntry(t.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:i??"",entryType:"tool_result",toolCallId:a,toolName:r})}}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t,s="string"==typeof e?.runId?e.runId:void 0;if(this.state.lastCronSignalAtMs=Date.now(),s){this.state.cronRunIds.add(s);const t=this.state.cronRunIdTimers.get(s);t&&clearTimeout(t);const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS,n=setTimeout(()=>{this.state.cronRunIds.delete(s),this.state.cronRunIdTimers.delete(s)},e);"object"==typeof n&&"unref"in n&&n.unref(),this.state.cronRunIdTimers.set(s,n)}this.pushCronWebhook(e,s);const n=this.state.findCronTargetSession();if(!n)return;const o=("string"==typeof e?.summary?e.summary:"")||("string"==typeof e?.text?e.text:"")||JSON.stringify(t),a=s?`run:${s}`:`signal:${Date.now()}`;this.bufferCronText(a,`[Cron] ${o}\n`,n.sessionId)}pushCronWebhook(t,e){if(!this.config.platformUrl||!this.agentFetchFn)return;const s=`${this.config.platformUrl}/api/v1/agent/webhooks/cron`,n=("string"==typeof t?.summary?t.summary:void 0)??("string"==typeof t?.text?t.text:void 0)??"",o={jobId:e??"",name:("string"==typeof t?.name?t.name:void 0)??("string"==typeof t?.jobName?t.jobName:void 0)??"",content:n,summary:n,type:"cron",action:"string"==typeof t?.action?t.action:"fired",status:"string"==typeof t?.status?t.status:""};this.agentFetchFn(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(t=>{this.logger.warn?.(`[gateway-events] cron webhook push failed: ${t instanceof Error?t.message:t}`)})}bufferCronText(t,e,s){const n=this.state.cronTextByKey.get(t)??"";this.state.cronTextByKey.set(t,n+e),this.scheduleCronFlush(t,s)}scheduleCronFlush(t,e){const s=this.state.cronFlushTimersByKey.get(t);s&&clearTimeout(s);const n=this.config.cronFlushTimeoutMs??DEFAULT_CRON_FLUSH_TIMEOUT_MS,o=setTimeout(()=>{this.state.cronFlushTimersByKey.delete(t),this.flushCronBufferByKey(t,e,"timeout")},n);"object"==typeof o&&"unref"in o&&o.unref(),this.state.cronFlushTimersByKey.set(t,o)}flushCronBufferByKey(t,e,s){const n=this.state.cronFlushTimersByKey.get(t);n&&(clearTimeout(n),this.state.cronFlushTimersByKey.delete(t));const o=(this.state.cronTextByKey.get(t)??"").trim();if(this.state.cronTextByKey.delete(t),!o)return void("timeout"===s&&this.clearCronStreamTracking(t));const a=this.state.getCronRequestId(t),r={sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}};this.transport.sendCronSessionUpdate(e,r,{requestId:a,messageType:"cron",timestamp:Date.now()});const i=this.state.sessions.get(e);i?.assistantMq?.push(r),"timeout"===s&&this.clearCronStreamTracking(t)}flushCronBufferForRun(t){if(!t)return;const e=`run:${t}`;if(!this.state.cronTextByKey.has(e))return;const s=this.state.findCronTargetSession();s&&this.flushCronBufferByKey(e,s.sessionId,"end")}clearCronStreamTracking(t){this.state.cronRequestIdsByKey.delete(t),this.state.cronTextByKey.delete(t);const e=this.state.cronFlushTimersByKey.get(t);if(e&&(clearTimeout(e),this.state.cronFlushTimersByKey.delete(t)),t.startsWith("run:")){const e=t.slice(4);if(e){this.state.cronRunIds.delete(e);const t=this.state.cronRunIdTimers.get(e);t&&(clearTimeout(t),this.state.cronRunIdTimers.delete(e))}}}handleUncorrelatedCronAgentEvent(t){if("lifecycle"===t.stream){const e=t.data,s="string"==typeof e?.phase?e.phase:void 0;if("end"===s){this.flushCronBufferForRun(t.runId);const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}else if("cancelled"===s||"cancel"===s||"error"===s){const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}return}if("assistant"!==t.stream)return;const e=this.state.findCronTargetSession();if(!e)return;const s=("string"==typeof t.data?.delta?t.data.delta:void 0)??("string"==typeof t.data?.text?t.data.text:void 0);if(!s)return;const n=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`;this.bufferCronText(n,s,e.sessionId)}handleUncorrelatedCronChatEvent(t){if("user"!==t.message?.role)return;const e=this.state.findCronTargetSession();if(e&&t.message?.content)for(const s of t.message.content)if("text"===s.type&&s.text){const n=stripTransportMetadata(s.text);if(!n)continue;const o=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`,a=this.state.getCronRequestId(o),r={sessionUpdate:"user_message_chunk",content:{type:"text",text:n}};this.transport.sendCronSessionUpdate(e.sessionId,r,{requestId:a,messageType:"cron",timestamp:Date.now()})}}}
1
+ import{Errors}from"../errors/index.js";import{stripTransportMetadata}from"../message-filter.js";import{appendHistoryEntry}from"./local-session-history.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS,DEFAULT_CRON_FLUSH_TIMEOUT_MS,DEFAULT_CRON_SIGNAL_WINDOW_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"!=typeof t)return;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||void 0}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return void(this.state.isCronCandidate(t.runId,e)&&this.handleUncorrelatedCronChatEvent(t))}if("number"==typeof t.seq){if(t.seq<=(e.lastChatSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate chat event seq=${t.seq} (last=${e.lastChatSeq}) runId=${t.runId}`);e.lastChatSeq=t.seq}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role)for(const o of n.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(a){if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=`chat:thinking:${t}`;if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},r=`chat:tool_start:${t}:${JSON.stringify(a)}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const r=`chat:tool_result:${t}:${a}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=`chat:text:${o.slice(0,128)}`;if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=o,o.startsWith(r))i=o.slice(r.length);else{if(r.startsWith(o))continue;i=o}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${r.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i}};this.transport.sendSessionUpdate(s,l),e.assistantMq?.push(l)}}}}else"delta"===t.state&&this.touchStreamActivity(e);this.maybeCompletePromptFromChatState(e,t),"final"===t.state?this.completePrompt(e,t.stopReason||"end_turn"):"aborted"===t.state||"cancelled"===t.state||"cancel"===t.state?this.completePrompt(e,"cancelled"):"error"===t.state&&this.failPrompt(e,Errors.AgentError.rpcCode,t.errorMessage||Errors.AgentError.message)}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0)??this.state.findCronTargetSession();if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return this.state.isCronCandidate(t.runId,e)?void this.handleUncorrelatedCronAgentEvent(t):void 0}if("number"==typeof t.seq){if(t.seq<=(e.lastAgentSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate agent event seq=${t.seq} (last=${e.lastAgentSeq}) runId=${t.runId} stream=${t.stream}`);e.lastAgentSeq=t.seq}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${o.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}}));const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:s,toolName:o});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});continue}}appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:o??""})}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=t.data?.delta||t.data?.text;if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:n,entryType:"thought"})}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:t,toolName:o})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool",a=this.resolveToolCallId(e,{...s,...t},"start");n({sessionUpdate:"tool_call",toolCallId:a,title:o,kind:"other",status:"in_progress"}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:"",entryType:"tool_call",toolCallId:a,toolName:o})}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){this.flushCronBufferForRun(t.runId);const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"end_turn")}else if("cancelled"===n||"cancel"===n){const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"cancelled")}else if("error"===n){const n=s?.error,o=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n&&"object"==typeof n?JSON.stringify(n):void 0)??"gateway lifecycle error";this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${o} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,o)}}}async handleToolResult(t,e){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,r=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",i="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:r,rawText:i,rawResult:e.data})}catch{c=i?[{type:"content",content:{type:"text",text:i}}]:[]}if(0===c.length&&i&&(c=[{type:"content",content:{type:"text",text:i}}]),c.length>0){const e=c.filter(t=>"text"===t.content?.type&&t.content?.text).map(t=>({type:"content",content:{type:"text",text:t.content.text}})),s={sessionUpdate:"tool_call_update",toolCallId:a,title:r,kind:"other",status:"completed",...e.length>0?{content:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s)&&t.assistantMq?.push(s),appendHistoryEntry(t.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:i??"",entryType:"tool_result",toolCallId:a,toolName:r})}}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t,s="string"==typeof e?.runId?e.runId:void 0;if(this.state.lastCronSignalAtMs=Date.now(),s){this.state.cronRunIds.add(s);const t=this.state.cronRunIdTimers.get(s);t&&clearTimeout(t);const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS,n=setTimeout(()=>{this.state.cronRunIds.delete(s),this.state.cronRunIdTimers.delete(s)},e);"object"==typeof n&&"unref"in n&&n.unref(),this.state.cronRunIdTimers.set(s,n)}this.pushCronWebhook(e,s);const n=this.state.findCronTargetSession();if(!n)return;const o=("string"==typeof e?.summary?e.summary:"")||("string"==typeof e?.text?e.text:"")||JSON.stringify(t),a=s?`run:${s}`:`signal:${Date.now()}`;this.bufferCronText(a,`[Cron] ${o}\n`,n.sessionId)}pushCronWebhook(t,e){if(!this.config.platformUrl||!this.agentFetchFn)return;const s=`${this.config.platformUrl}/api/v1/agent/webhooks/cron`,n=("string"==typeof t?.summary?t.summary:void 0)??("string"==typeof t?.text?t.text:void 0)??"",o={jobId:e??"",name:("string"==typeof t?.name?t.name:void 0)??("string"==typeof t?.jobName?t.jobName:void 0)??"",content:n,summary:n,type:"cron",action:"string"==typeof t?.action?t.action:"fired",status:"string"==typeof t?.status?t.status:""};this.agentFetchFn(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(t=>{this.logger.warn?.(`[gateway-events] cron webhook push failed: ${t instanceof Error?t.message:t}`)})}bufferCronText(t,e,s){const n=this.state.cronTextByKey.get(t)??"";this.state.cronTextByKey.set(t,n+e),this.scheduleCronFlush(t,s)}scheduleCronFlush(t,e){const s=this.state.cronFlushTimersByKey.get(t);s&&clearTimeout(s);const n=this.config.cronFlushTimeoutMs??DEFAULT_CRON_FLUSH_TIMEOUT_MS,o=setTimeout(()=>{this.state.cronFlushTimersByKey.delete(t),this.flushCronBufferByKey(t,e,"timeout")},n);"object"==typeof o&&"unref"in o&&o.unref(),this.state.cronFlushTimersByKey.set(t,o)}flushCronBufferByKey(t,e,s){const n=this.state.cronFlushTimersByKey.get(t);n&&(clearTimeout(n),this.state.cronFlushTimersByKey.delete(t));const o=(this.state.cronTextByKey.get(t)??"").trim();if(this.state.cronTextByKey.delete(t),!o)return void("timeout"===s&&this.clearCronStreamTracking(t));const a=this.state.getCronRequestId(t),r={sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}};this.transport.sendCronSessionUpdate(e,r,{requestId:a,messageType:"cron",timestamp:Date.now()});const i=this.state.sessions.get(e);i?.assistantMq?.push(r),"timeout"===s&&this.clearCronStreamTracking(t)}flushCronBufferForRun(t){if(!t)return;const e=`run:${t}`;if(!this.state.cronTextByKey.has(e))return;const s=this.state.findCronTargetSession();s&&this.flushCronBufferByKey(e,s.sessionId,"end")}clearCronStreamTracking(t){this.state.cronRequestIdsByKey.delete(t),this.state.cronTextByKey.delete(t);const e=this.state.cronFlushTimersByKey.get(t);if(e&&(clearTimeout(e),this.state.cronFlushTimersByKey.delete(t)),t.startsWith("run:")){const e=t.slice(4);if(e){this.state.cronRunIds.delete(e);const t=this.state.cronRunIdTimers.get(e);t&&(clearTimeout(t),this.state.cronRunIdTimers.delete(e))}}}handleUncorrelatedCronAgentEvent(t){if("lifecycle"===t.stream){const e=t.data,s="string"==typeof e?.phase?e.phase:void 0;if("end"===s){this.flushCronBufferForRun(t.runId);const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}else if("cancelled"===s||"cancel"===s||"error"===s){const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}return}if("assistant"!==t.stream)return;const e=this.state.findCronTargetSession();if(!e)return;const s=("string"==typeof t.data?.delta?t.data.delta:void 0)??("string"==typeof t.data?.text?t.data.text:void 0);if(!s)return;const n=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`;this.bufferCronText(n,s,e.sessionId)}handleUncorrelatedCronChatEvent(t){if("user"!==t.message?.role)return;const e=this.state.findCronTargetSession();if(e&&t.message?.content)for(const s of t.message.content)if("text"===s.type&&s.text){const n=stripTransportMetadata(s.text);if(!n)continue;const o=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`,a=this.state.getCronRequestId(o),r={sessionUpdate:"user_message_chunk",content:{type:"text",text:n}};this.transport.sendCronSessionUpdate(e.sessionId,r,{requestId:a,messageType:"cron",timestamp:Date.now()})}}}
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal-session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal-bridge-handler.js";import{CanvasActionRouter,formatActionForAgent}from"./canvas-action-router.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{FormSurfaceBuilder}from"./form-surface-builder.js";import{ResourceSurfaceRenderer}from"./resource-surface-renderer.js";const SIMPLE_REMAP={Callout:"Alert",Carousel:"List"};function sanitizeA2UIComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e,s=String(t.type??""),n=SIMPLE_REMAP[s];return n?{...t,type:n}:t})}export class ACPServer{agentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch{}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n){this.agentFetchFn=n,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),void 0===e.pendingPromptId&&e.currentRunId&&(e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.promptDone=void 0),this.actionRouter?.drainPending().catch(e=>{this.logger.warn?.(`[acp-server] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)})},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),void 0===e.pendingPromptId&&e.currentRunId&&(e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.promptDone=void 0),this.actionRouter?.clearPending()},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.handleCanvasNotificationInternal(e,t)},this.core.getCanvasState=()=>this.canvasState,this.core.handleSlashCommand=async e=>{const t=this.descriptorRegistry?.getBySlashCommand(e);return!(!t||!this.resourceRenderer)&&(await this.resourceRenderer.render(t,"inline"),!0)},this.core.onPromptStart=()=>{this.actionRouter?.clearPending()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e))}async start(){const e=this.config.bridge.url,t=this.config.bridge.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),await this.connectGateway()}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});s.on("connected",()=>{this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"})}),s.on("disconnected",(...e)=>{const t=String(e[0]??"unknown");this.logger.warn?.(`[acp-server] Bridge disconnected: ${t}`),this.core.initialized=!1}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n,this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}isCronCorrelatedRun(e){return this.core.isCronCorrelatedRun(e)}setCanvasState(e,t){this.canvasState=e;const s=(e,t)=>this.sendCanvasUpdate(e,t),n=async(e,t)=>{if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)},r=t?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,sendCanvasUpdate:s,gatewayRequest:n,logger:this.logger});this.formBuilder=i,this.schemaRegistry=r,this.descriptorRegistry=t?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,sendCanvasUpdate:s,gatewayRequest:n,schemaRegistry:r,formBuilder:i,logger:this.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.logger,writeObs:e=>this.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e)})}sendCanvasUpdate(e,t){const s=e&&"_active"!==e&&"_openclaw"!==e?e:this.state.lastActiveSessionId,n=s?this.state.sessions.get(s):void 0;if(!1===n?.canvasActive)return;const r=n?.sessionId??s;r&&this.transport.sendSessionUpdate(r,t)}isCanvasActive(e){const t=e??this.state.lastActiveSessionId,s=t?this.state.sessions.get(t):void 0;return!1!==s?.canvasActive}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=e.split("_");if(s.length>=3&&"tc"===s[0]){const e=s[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}for(const[,e]of this.state.sessions)if(void 0!==e.pendingPromptId&&!e.promptDone)return e.sessionId;return this.state.lastActiveSessionId}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}getResourceRenderer(){return this.resourceRenderer}getDescriptorRegistry(){return this.descriptorRegistry}getFormBuilder(){return this.formBuilder}getSchemaRegistry(){return this.schemaRegistry}sendCanvasReset(e,t){const s=t??this.state.lastActiveSessionId,n=s?this.state.sessions.get(s):void 0,r=n?.sessionId??s;if(!r)return;const i={sessionUpdate:"canvas.reset"};e&&(i.surfaceId=e),this.transport.sendSessionUpdate(r,i)}async forwardActionToAgent(e){const t=this.state.resolveSessionFromParams(e);if(!t)return void this.logger.warn?.("[acp-server] forwardActionToAgent dropped — no session resolved from params");if(!this.gateway?.isConnected)return;if(t.currentRunId&&!t.promptDone)return this.actionRouter?.enqueue(e),void this.logger.debug?.("[acp-server] Agent busy, canvas action queued");const s=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,s??void 0);try{const e=await this.gateway.agentSend(t.gatewaySessionKey,n,this.config.gateway.agentId||"main");t.currentRunId=e.runId,t.assistantStreamOwnerRunId=e.runId,t.promptDone=!1}catch(s){this.logger.warn?.(`[acp-server] Agent forward failed: ${s instanceof Error?s.message:s}`),this.transport.sendSessionUpdate(t.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}handleCanvasNotificationInternal(e,t){if(!this.canvasState)return void this.logger.warn?.(`[acp-server] Canvas notification ${e} ignored — canvasState not initialized (canvasEnabled=${this.config.canvasEnabled})`);this.canvasState,this.logger;if("canvas/action"===e)this.actionRouter?.route(t).catch(e=>{this.logger.warn?.(`[acp-server] Canvas action routing failed: ${e instanceof Error?e.message:e}`)});else if("canvas/toggle"===e){const e=t,s=!1!==e.enabled,n=this.state.resolveSessionFromParams(e);n&&(n.canvasActive=s,s&&(n.canvasPromptInjected=!1)),this.logger.info?.(`[acp-server] Canvas toggled: ${s}`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:s}})}else if("canvas/clear"===e){const e=t,s="string"==typeof e?.sessionId?e.sessionId:void 0,n=this.canvasState.getAllActive().length;this.canvasState.clear(),this.actionRouter?.reset(),this.sendCanvasReset(void 0,s),this.logger.info?.(`[acp-server] Canvas cleared (${n} surfaces removed)`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}else"canvas/delete"===e&&this.logger.debug?.("[acp-server] canvas/delete via ACP is deprecated — deletions should go through Relay")}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const t=e,s=t.action;if("a2ui_push"===s||"surface_update"===s){const e=String(t.surfaceId??`openclaw-${Date.now()}`),s=t.components;if(s&&Array.isArray(s)){const t=sanitizeA2UIComponents(s);this.canvasState.has(e)?this.canvasState.update(e,t):this.canvasState.create(e,"inline",t,{origin:{kind:"openclaw"}}),this.logger.debug?.(`[acp-server] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===s||"url_open"===s){const e=String(t.url??"");if(e){const s=`openclaw-nav-${Date.now()}`,n=[{id:`${s}-embed`,type:"WebEmbed",properties:{url:e,title:String(t.title??e)}}];this.canvasState.create(s,"inline",n,{origin:{kind:"openclaw"}})}}else this.logger.debug?.(`[acp-server] Unhandled OpenClaw canvas action: ${s}`)}async stop(){this.state.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop()),this.client&&(this.client.removeAllListeners(),this.client.close()),this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close()),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
1
+ import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal-session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal-bridge-handler.js";import{CanvasActionRouter,formatActionForAgent}from"./canvas-action-router.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{FormSurfaceBuilder}from"./form-surface-builder.js";import{ResourceSurfaceRenderer}from"./resource-surface-renderer.js";const SIMPLE_REMAP={Callout:"Alert",Carousel:"List"};function sanitizeA2UIComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e,s=String(t.type??""),n=SIMPLE_REMAP[s];return n?{...t,type:n}:t})}export class ACPServer{agentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch{}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n){this.agentFetchFn=n,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),void 0===e.pendingPromptId&&e.currentRunId&&(e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.promptDone=void 0),this.actionRouter?.drainPending().catch(e=>{this.logger.warn?.(`[acp-server] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)})},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),void 0===e.pendingPromptId&&e.currentRunId&&(e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.promptDone=void 0),this.actionRouter?.clearPending()},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.handleCanvasNotificationInternal(e,t)},this.core.getCanvasState=()=>this.canvasState,this.core.handleSlashCommand=async e=>{const t=this.descriptorRegistry?.getBySlashCommand(e);return!(!t||!this.resourceRenderer)&&(await this.resourceRenderer.render(t,"inline"),!0)},this.core.onPromptStart=()=>{this.actionRouter?.clearPending()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e))}async start(){const e=this.config.bridge.url,t=this.config.bridge.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),await this.connectGateway()}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});s.on("connected",()=>{this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"})}),s.on("disconnected",(...e)=>{const t=String(e[0]??"unknown");this.logger.warn?.(`[acp-server] Bridge disconnected: ${t}`),this.core.initialized=!1}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n,this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}isCronCorrelatedRun(e){return this.core.isCronCorrelatedRun(e)}setCanvasState(e,t){this.canvasState=e;const s=(e,t)=>this.sendCanvasUpdate(e,t),n=async(e,t)=>{if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)},r=t?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,sendCanvasUpdate:s,gatewayRequest:n,logger:this.logger});this.formBuilder=i,this.schemaRegistry=r,this.descriptorRegistry=t?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,sendCanvasUpdate:s,gatewayRequest:n,schemaRegistry:r,formBuilder:i,logger:this.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.logger,writeObs:e=>this.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e)})}sendCanvasUpdate(e,t){const s=e&&"_active"!==e&&"_openclaw"!==e?e:this.state.lastActiveSessionId,n=s?this.state.sessions.get(s):void 0;if(!1===n?.canvasActive)return;const r=n?.sessionId??s;r&&this.transport.sendSessionUpdate(r,t)}isCanvasActive(e){const t=e??this.state.lastActiveSessionId,s=t?this.state.sessions.get(t):void 0;return!1!==s?.canvasActive}isCanvasActiveForGatewayKey(e){const t=this.state.sessionsByGatewayKey.get(e);return!1!==t?.canvasActive}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=e.split("_");if(s.length>=3&&"tc"===s[0]){const e=s[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}for(const[,e]of this.state.sessions)if(void 0!==e.pendingPromptId&&!e.promptDone)return e.sessionId;return this.state.lastActiveSessionId}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}getResourceRenderer(){return this.resourceRenderer}getDescriptorRegistry(){return this.descriptorRegistry}getFormBuilder(){return this.formBuilder}getSchemaRegistry(){return this.schemaRegistry}sendCanvasReset(e,t){const s=t??this.state.lastActiveSessionId,n=s?this.state.sessions.get(s):void 0,r=n?.sessionId??s;if(!r)return;const i={sessionUpdate:"canvas.reset"};e&&(i.surfaceId=e),this.transport.sendSessionUpdate(r,i)}async forwardActionToAgent(e){const t=this.state.resolveSessionFromParams(e);if(!t)return void this.logger.warn?.("[acp-server] forwardActionToAgent dropped — no session resolved from params");if(!this.gateway?.isConnected)return;if(t.currentRunId&&!t.promptDone)return this.actionRouter?.enqueue(e),void this.logger.debug?.("[acp-server] Agent busy, canvas action queued");const s=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,s??void 0);try{const e=await this.gateway.agentSend(t.gatewaySessionKey,n,this.config.gateway.agentId||"main");t.currentRunId=e.runId,t.assistantStreamOwnerRunId=e.runId,t.promptDone=!1}catch(s){this.logger.warn?.(`[acp-server] Agent forward failed: ${s instanceof Error?s.message:s}`),this.transport.sendSessionUpdate(t.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}handleCanvasNotificationInternal(e,t){if(!this.canvasState)return void this.logger.warn?.(`[acp-server] Canvas notification ${e} ignored — canvasState not initialized (canvasEnabled=${this.config.canvasEnabled})`);this.canvasState,this.logger;if("canvas/action"===e)this.actionRouter?.route(t).catch(e=>{this.logger.warn?.(`[acp-server] Canvas action routing failed: ${e instanceof Error?e.message:e}`)});else if("canvas/toggle"===e){const e=t,s=!1!==e.enabled,n=this.state.resolveSessionFromParams(e);n&&(n.canvasActive=s,s&&(n.canvasPromptInjected=!1)),this.logger.info?.(`[acp-server] Canvas toggled: ${s}`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:s}})}else if("canvas/clear"===e){const e=t,s="string"==typeof e?.sessionId?e.sessionId:void 0,n=this.canvasState.getAllActive().length;this.canvasState.clear(),this.actionRouter?.reset(),this.sendCanvasReset(void 0,s),this.logger.info?.(`[acp-server] Canvas cleared (${n} surfaces removed)`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}else"canvas/delete"===e&&this.logger.debug?.("[acp-server] canvas/delete via ACP is deprecated — deletions should go through Relay")}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const t=e,s=t.action;if("a2ui_push"===s||"surface_update"===s){const e=String(t.surfaceId??`openclaw-${Date.now()}`),s=t.components;if(s&&Array.isArray(s)){const t=sanitizeA2UIComponents(s);this.canvasState.has(e)?this.canvasState.update(e,t):this.canvasState.create(e,"inline",t,{origin:{kind:"openclaw"}}),this.logger.debug?.(`[acp-server] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===s||"url_open"===s){const e=String(t.url??"");if(e){const s=`openclaw-nav-${Date.now()}`,n=[{id:`${s}-embed`,type:"WebEmbed",properties:{url:e,title:String(t.title??e)}}];this.canvasState.create(s,"inline",n,{origin:{kind:"openclaw"}})}}else this.logger.debug?.(`[acp-server] Unhandled OpenClaw canvas action: ${s}`)}async stop(){this.state.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop()),this.client&&(this.client.removeAllListeners(),this.client.close()),this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close()),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
@@ -1 +1 @@
1
- export const Errors={MethodNotFound:{rpcCode:-32601,code:"method_not_found",message:"Method not found"},InvalidParams:{rpcCode:-32602,code:"invalid_params",message:"Invalid params"},MissingParam:{rpcCode:-32602,code:"missing_param",message:"Missing required parameter"},InternalError:{rpcCode:-32603,code:"internal_error",message:"Internal error"},GatewayNotConnected:{rpcCode:-32001,code:"gateway_not_connected",message:"Gateway not connected"},GatewayDisconnected:{rpcCode:-32001,code:"gateway_disconnected",message:"Gateway disconnected"},SendFailed:{rpcCode:-32001,code:"send_failed",message:"Failed to send to gateway"},SessionNotFound:{rpcCode:-32602,code:"session_not_found",message:"Unknown session"},SessionDeleted:{rpcCode:-32001,code:"session_deleted",message:"Session deleted"},PromptConvertFailed:{rpcCode:-32602,code:"prompt_convert_failed",message:"Prompt conversion failed"},PromptTimeout:{rpcCode:-32022,code:"prompt_timeout",message:"Prompt timed out"},ShellDisabled:{rpcCode:-32010,code:"shell_disabled",message:"Shell is disabled"},TerminalNotFound:{rpcCode:-32011,code:"terminal_not_found",message:"Terminal not found"},TerminalClosed:{rpcCode:-32012,code:"terminal_closed",message:"Terminal already closed"},TerminalQuotaExceeded:{rpcCode:-32013,code:"terminal_quota_exceeded",message:"Terminal quota exceeded"},TerminalTimeout:{rpcCode:-32014,code:"terminal_timeout",message:"Terminal timeout"},GeneralError:{rpcCode:-32e3,code:"general_error",message:"An error occurred"},Unauthorized:{rpcCode:0,code:"unauthorized",message:"Unauthorized"},InvalidBody:{rpcCode:0,code:"invalid_body",message:"Invalid JSON body"},MissingField:{rpcCode:0,code:"missing_field",message:"Required field missing"},FsError:{rpcCode:0,code:"fs_error",message:"Filesystem error"},DownloadFailed:{rpcCode:0,code:"download_failed",message:"Download failed"}};
1
+ export const Errors={MethodNotFound:{rpcCode:-32601,code:"method_not_found",message:"Method not found"},InvalidParams:{rpcCode:-32602,code:"invalid_params",message:"Invalid params"},MissingParam:{rpcCode:-32602,code:"missing_param",message:"Missing required parameter"},InternalError:{rpcCode:-32603,code:"internal_error",message:"Internal error"},GatewayNotConnected:{rpcCode:-32001,code:"gateway_not_connected",message:"Gateway not connected"},GatewayDisconnected:{rpcCode:-32001,code:"gateway_disconnected",message:"Gateway disconnected"},SendFailed:{rpcCode:-32001,code:"send_failed",message:"Failed to send to gateway"},SessionNotFound:{rpcCode:-32602,code:"session_not_found",message:"Unknown session"},SessionDeleted:{rpcCode:-32001,code:"session_deleted",message:"Session deleted"},PromptConvertFailed:{rpcCode:-32602,code:"prompt_convert_failed",message:"Prompt conversion failed"},PromptTimeout:{rpcCode:-32022,code:"prompt_timeout",message:"Prompt timed out"},ShellDisabled:{rpcCode:-32010,code:"shell_disabled",message:"Shell is disabled"},TerminalNotFound:{rpcCode:-32011,code:"terminal_not_found",message:"Terminal not found"},TerminalClosed:{rpcCode:-32012,code:"terminal_closed",message:"Terminal already closed"},TerminalQuotaExceeded:{rpcCode:-32013,code:"terminal_quota_exceeded",message:"Terminal quota exceeded"},TerminalTimeout:{rpcCode:-32014,code:"terminal_timeout",message:"Terminal timeout"},AgentError:{rpcCode:-32021,code:"agent_error",message:"Agent error"},GeneralError:{rpcCode:-32e3,code:"general_error",message:"An error occurred"},Unauthorized:{rpcCode:0,code:"unauthorized",message:"Unauthorized"},InvalidBody:{rpcCode:0,code:"invalid_body",message:"Invalid JSON body"},MissingField:{rpcCode:0,code:"missing_field",message:"Required field missing"},FsError:{rpcCode:0,code:"fs_error",message:"Filesystem error"},DownloadFailed:{rpcCode:0,code:"download_failed",message:"Download failed"}};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload-result-cache.js";import{handleFileStage}from"./file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal-websocket.js";import{TerminalManager}from"./terminal-session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{CanvasStateManager}from"./canvas-state.js";import{CanvasApiClient}from"./canvas-api-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas-tools.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./render-form-tool.js";import{ResourceDescriptorRegistry}from"./resource-descriptor-registry.js";import{CRON_RESOURCE}from"./cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./action-tool-generator.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return void e.logger.error?.("[beeos-claw] Failed to resolve config:",r)}const a=o.config;pluginConfig=a;const t=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};t({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),t({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)t({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.bridge.keyFile)try{agentKeys=loadAgentKeyPair(a.bridge.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const n=await executeUpload(o,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:r,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),n.ok&&n.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,n.content),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,r){await handleFileStage(e,r,a)}});const n=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0;if(acpServer=new ACPServer(a,e.logger,t,n),e.registerService({id:"beeos-acp",async start(){await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}}),a.canvasEnabled){const r=new CanvasStateManager;let o=null;a.platformUrl&&agentKeys?(o=new CanvasApiClient({platformUrl:a.platformUrl,agentKeys:agentKeys}),r.setRelayOpts({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,logger:e.logger}),e.logger.info?.("[beeos-claw] Canvas API client + Relay opts configured")):e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: platformUrl or agentKeys missing");const n={stateManager:r,sendCanvasUpdate:(e,r)=>{acpServer.sendCanvasUpdate(e,r)},logger:e.logger,writeObs:e=>t(e),isCanvasActive:()=>acpServer.isCanvasActive(),canvasApiClient:o,getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e)},s=new FormSchemaRegistry;s.register(CRON_CREATE_SCHEMA),s.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE),e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:createRenderUiHandler(n)}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:createCloseUiHandler(n)}),acpServer.setCanvasState(r,{schemaRegistry:s,descriptorRegistry:i});const l=acpServer.getFormBuilder();l&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(s),parameters:RENDER_FORM_TOOL_SCHEMA,execute:createRenderFormHandler({registry:s,builder:l,logger:e.logger})});const g=acpServer.getResourceRenderer();if(g){generateActionTools(i,e,async(e,r)=>{const o=acpServer?.gateway;if(!o?.isConnected)throw new Error("Gateway not connected");return o.request(e,r)},g,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(i),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:createRenderResourceHandler(i,g,e.logger)})}if("function"==typeof e.on)try{e.on("before_prompt_build",()=>acpServer.isCanvasActive()?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is disabled by user. Do NOT use render_ui or close_ui. Respond with plain text only."}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const s=new TerminalManager,i=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:s,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){i.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){i.stop(),s.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r){const o=mirrorOpenClawConfig(r);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload-result-cache.js";import{handleFileStage}from"./file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal-websocket.js";import{TerminalManager}from"./terminal-session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{CanvasStateManager}from"./canvas-state.js";import{CanvasApiClient}from"./canvas-api-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas-tools.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./render-form-tool.js";import{ResourceDescriptorRegistry}from"./resource-descriptor-registry.js";import{CRON_RESOURCE}from"./cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./action-tool-generator.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return void e.logger.error?.("[beeos-claw] Failed to resolve config:",r)}const a=o.config;pluginConfig=a;const t=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};t({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),t({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)t({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.bridge.keyFile)try{agentKeys=loadAgentKeyPair(a.bridge.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),t({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s.ok&&s.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,s.content),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,r){await handleFileStage(e,r,a)}});const s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0;if(acpServer=new ACPServer(a,e.logger,t,s),e.registerService({id:"beeos-acp",async start(){await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}}),a.canvasEnabled){const r=new CanvasStateManager;let o=null;a.platformUrl&&agentKeys?(o=new CanvasApiClient({platformUrl:a.platformUrl,agentKeys:agentKeys}),r.setRelayOpts({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,logger:e.logger}),e.logger.info?.("[beeos-claw] Canvas API client + Relay opts configured")):e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: platformUrl or agentKeys missing");const s={stateManager:r,sendCanvasUpdate:(e,r)=>{acpServer.sendCanvasUpdate(e,r)},logger:e.logger,writeObs:e=>t(e),isCanvasActive:()=>acpServer.isCanvasActive(),canvasApiClient:o,getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e)},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE),e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:createRenderUiHandler(s)}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:createCloseUiHandler(s)}),acpServer.setCanvasState(r,{schemaRegistry:n,descriptorRegistry:i});const l=acpServer.getFormBuilder();l&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:createRenderFormHandler({registry:n,builder:l,logger:e.logger})});const c=acpServer.getResourceRenderer();if(c){generateActionTools(i,e,async(e,r)=>{const o=acpServer?.gateway;if(!o?.isConnected)throw new Error("Gateway not connected");return o.request(e,r)},c,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(i),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:createRenderResourceHandler(i,c,e.logger)})}const g=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]);if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(!g.has(e.toolName))return;return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const n=new TerminalManager,i=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:n,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){i.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){i.stop(),n.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r){const o=mirrorOpenClawConfig(r);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
@@ -1 +1 @@
1
- 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,t){const r=e[t];if("string"==typeof r){const e=r.trim();return e.length>0?e:void 0}}function readPositiveInt(e,t){const r=e[t];if("number"==typeof r&&Number.isInteger(r)&&r>0)return r}function invalid(e,t){return{ok:!1,error:{...TERMINAL_INVALID_PARAMS,data:{field:e,reason:t}}}}function parseBase(e){return isRecord(e)?{ok:!0,value:e}:invalid("params","must be an object")}export function parseTerminalOpenParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readPositiveInt(r,"cols")??80,a=readPositiveInt(r,"rows")??24,i=readString(r,"sessionId"),s=readString(r,"command"),o=readString(r,"cwd");return{ok:!0,value:{sessionId:i,command:s,cols:n,rows:a,...o?{cwd:o}:{}}}}export function parseTerminalInputParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(r,"data"),i=readString(r,"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 t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(r,"cols");if(!a)return invalid("cols","must be a positive integer");const i=readPositiveInt(r,"rows");return i?{ok:!0,value:{terminalId:n,cols:a,rows:i}}:invalid("rows","must be a positive integer")}export function parseTerminalCloseParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(r,"signal");return{ok:!0,value:{terminalId:n,...a?{signal:a}:{}}}}export function parseTerminalWaitParams(e){const t=parseBase(e);if(!t.ok)return t;const r=t.value,n=readString(r,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(r,"timeoutMs");return{ok:!0,value:{terminalId:n,...a?{timeoutMs:a}:{}}}}export function isTerminalRpcMethod(e){return Object.values(TERMINAL_METHODS).includes(e)}
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)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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>",