grix-connector
Version:
Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.
14 lines (12 loc) • 85.3 kB
JavaScript
import{spawn as W,spawnSync as Ce,execSync as j,execFile as ke}from"node:child_process";import{promisify as Ie}from"node:util";import{randomUUID as H}from"node:crypto";import{killProcessGroup as N,hasChildProcesses as Ae}from"../../core/runtime/spawn.js";import{readdirSync as be,readFileSync as Q,realpathSync as Te,rmSync as $e,statSync as L,existsSync as D,openSync as Re,writeSync as De,closeSync as Me,constants as ie,watch as xe}from"node:fs";import{mkdir as x,readFile as w,rm as se,stat as ne,writeFile as C}from"node:fs/promises";import{join as h,resolve as Le}from"node:path";import{homedir as M,tmpdir as Oe}from"node:os";import V from"node:net";import{EventEmitter as re}from"node:events";import{fileURLToPath as Ne}from"node:url";let U=null;if(process.platform==="win32")try{U=await import("node-pty")}catch{U=null}import{resolveRuntimePaths as E}from"../../core/config/index.js";import{SESSION_MODE_IDS as F}from"./protocol-contract.js";import{log as a}from"../../core/log/index.js";import{ActivityStatusManager as Fe}from"./activity-status-manager.js";import{HookSignalStore as je}from"../../core/hooks/hook-signal-store.js";import{QuestionStore as ae}from"../../core/persistence/question-store.js";import{PermissionStore as q}from"../../core/persistence/permission-store.js";import{InternalApiServer as He}from"../../core/mcp/internal-api-server.js";import{executeEventTool as Ue,isEventTool as qe}from"../../core/mcp/event-tool-executor.js";import{validateToolArgs as Be}from"../../core/mcp/tool-schemas.js";import{ACCESS_CONTROL_ACTION_MAP as Ge,isGrixInternalToolName as Je,normalizeEventToolArgs as ze}from"../../core/mcp/tools.js";import{scanSkills as We}from"./skill-scanner.js";import{syncDefaultSkillsToDir as Qe}from"../../default-skills/index.js";import{extractLastAssistantEntry as B,extractLastAssistantText as Ve,hasTerminalToolResultAfterOffset as oe,probeSessionTurnState as le,resolveSessionJsonlPath as Y}from"./usage-parser.js";import{readSettingsEnv as Ye}from"./model-list.js";import{resolveCliPath as Xe,getCliVersion as Ke}from"../../core/util/cli-probe.js";const O="grix";function Ze(d){if(!Array.isArray(d))return;const e=d.map(t=>{if(typeof t=="string")return t.trim();if(!t||typeof t!="object")return"";const i=t,s=String(i.label??"").trim();if(s)return s;const n=String(i.value??"").trim();if(n)return n;const r=String(i.text??"").trim();return r||""}).filter(t=>t.length>0);return e.length>0?e:void 0}function ce(d){let e;try{e=JSON.parse(d)}catch{return null}const t=e.questions;if(!Array.isArray(t)||t.length===0)return null;const i=[];for(let s=0;s<t.length;s++){const n=t[s];if(!n||typeof n!="object")continue;const r=n,o=`Question ${s+1}`,l=String(r.header??r.question??o).trim()||o,c=String(r.prompt??r.question??"").trim();if(!c)continue;const u=Ze(r.options),f=r.multiSelect===!0||r.multi_select===!0;i.push({header:l,prompt:c,...u?{options:u}:{},...f?{multi_select:!0}:{}})}return i.length>0?i:null}function et(d){if(!d||typeof d!="string")return null;try{const e=JSON.parse(d);return(typeof e.plan=="string"?e.plan.trim():"")||null}catch{return null}}const tt=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],it=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],st=/API Error:\s*400.*server_tool_use\.id/,de=300*1e3,nt=1800*1e3,ue=60*1e3,he=600*1e3,rt=300*1e3,at="The model's tool call could not be parsed (retry also failed).",ot="[\u7CFB\u7EDF\u81EA\u52A8\u91CD\u8BD5] \u4F60\u4E0A\u4E00\u6761\u56DE\u590D\u7684\u5DE5\u5177\u8C03\u7528\u89E3\u6790\u5931\u8D25\uFF0C\u6CA1\u6709\u53D1\u51FA\u53BB\u3002\u8BF7\u4E0D\u8981\u91CD\u590D\u6267\u884C\u5DF2\u7ECF\u5B8C\u6210\u7684\u64CD\u4F5C\uFF0C\u76F4\u63A5\u91CD\u65B0\u8C03\u7528 reply \u5DE5\u5177\uFF0C\u628A\u4F60\u4E0A\u4E00\u6761\u60F3\u53D1\u7ED9\u7528\u6237\u7684\u7ED3\u8BBA\u539F\u6837\u91CD\u65B0\u53D1\u9001\u4E00\u6B21\u3002",lt="Sorry, the previous reply could not be generated properly (the model's tool call failed to parse). Please send it again, and use /grix restart to restart the session if needed.",ct=2*1e3,X=90*1e3,pe=1800*1e3,fe=15*1e3,dt=300*1e3,me=10*1e3,ut=30*1e3,ht=30*1e3;function pt(d){try{return process.kill(d,0),!0}catch(e){return e.code==="EPERM"}}const ft=12e4,mt=3e4,vt=3e3,G=1e3,ve=45e3,gt=18e3,K=8192;let ge=!1;const Z=new Set;async function yt(d=[]){const e=new Set(d.filter(t=>Number.isInteger(t)&&t>0));for(let t=0;t<20;t++){const i=await new Promise((s,n)=>{const r=V.createServer();r.once("error",n),r.listen(0,"127.0.0.1",()=>{const o=r.address(),l=typeof o=="object"&&o?o.port:0;r.close(c=>{if(c){n(c);return}s(l)})})});if(i>0&&!e.has(i)&&!Z.has(i))return Z.add(i),i}throw new Error("\u65E0\u6CD5\u5206\u914D MCP \u901A\u77E5\u7AEF\u53E3")}function St(d){d>0&&Z.delete(d)}const ye=["You are connected to a chat via the grix-claude MCP server.",'Messages arrive as <channel source="grix-claude" chat_id="..." event_id="..." message_id="..." user_id="...">text</channel>.',"When present, the channel metadata includes context_messages_json \u2014 a JSON array of earlier visible messages for this trigger; each entry carries sender_id and content. An entry whose content starts with [\u5F15\u7528\u6D88\u606F] is the quoted (replied-to) message being responded to. Use these records as context (prefer them over inferring from quoted_message_id alone), in group chats use sender_id to tell who said what, and only answer the current message.","IMPORTANT: You MUST use the reply tool to send any response to the user.","Your plain text output is NOT delivered to the chat \u2014 only the reply tool delivers messages.","Always call the reply tool with chat_id, event_id, and your response text.","If you intentionally do not want to send a visible reply, you must call the complete tool with event_id and a final status.",'Do not send a bare acknowledgement or "got it / on it / starting now" message before doing the work; do the work first, then send a single reply with the actual result. Only send an intermediate reply when a genuinely long-running task needs a progress update.',"You only run while handling a turn; once your turn ends you are frozen and cannot send anything until the user messages you again.",'Unbreakable rule: never end a turn having promised something you cannot deliver. In particular, never start a task, detach it (nohup, a trailing &, any background process or watcher), end the turn, and promise to "notify when it finishes" \u2014 that notification can never happen, so do not promise it.',"For any task whose result the user is waiting on (deploy, build, rollout, tests), run it in the foreground and wait for it to finish within this same turn, then report the real outcome. You may launch several in the background at once for parallelism, but you must still collect all of their results within this same turn before you reply and finish.","Two cases where leaving a process running is correct: a long-lived service that is meant to keep running (dev server, daemon) \u2014 background it, then report that it started plus how to check or stop it; and a task too long to finish in one turn \u2014 you may leave it running, but state plainly that you cannot auto-notify and give the user a concrete way to retrieve the result later (a log path, a PID, or asking them to ping you to re-check)."].join(" ");function Se(d){return String(d??"").trim().toLowerCase()===F.approval?F.approval:F.fullAuto}class we extends re{type="claude";config;bridgeCallbacks;mcpServerProcess=null;internalApi=null;claudeProcess=null;claudePty=null;spawnPromise=null;lifecycleVersion=0;sessionId="";alive=!1;stopped=!1;activeEvent=null;activeEventIdleTimer=null;activeEventHardTimer=null;activeEventPostReplyTimer=null;activeEventPostReplyWatcher=null;activeEventPostReplyPoll=null;stopHookBarrierSessionId=null;stopHookBarrierTimer=null;selfDrivenActive=!1;selfDrivenLastSignalAt=0;selfDrivenSweepTimer=null;selfDrivenReported=!1;selfDrivenReportTimer=null;compacting=!1;compactingTimer=null;compactionDoneResolver=null;mcpServerReady=!1;mcpStartupFailureHandled=!1;mcpChannelBroken=!1;channelGateClosed=!1;startupChannelListening=!1;startupChannelListeningAt=0;pendingMcpFailureTimer=null;sessionIdConflictDetected=!1;sessionIdConflictRetriedEventIds=new Set;malformedToolRetriedEventIds=new Set;cachedCliVersion=null;runtimeResolver=null;activityManager=null;lastPreToolInput="";deferredModelId=null;claudeChildPid=0;claudeCliSessionId="";claudeSessionCwd="";claudeMcpConfigPath="";expectRunDir="";sessionState=null;authFailureUntil=0;usageLimitUntil=0;completedEventIds=new Map;pendingQuestion=new Map;pendingPermissions=new Map;lastClearedEvent=null;constructor(e,t){super(),this.config=e,this.bridgeCallbacks=t;const s=(e.options??{}).sessionRuntimeResolver;typeof s=="function"&&(this.runtimeResolver=s)}async start(){this.lifecycleVersion+=1,this.stopped=!1,this.alive=!0,this.internalApi=new He,this.internalApi.setInvokeHandler(async(e,t,i)=>this.handleInternalInvoke(e,t,i)),this.internalApi.setStatusLineHandler(async e=>{this.handleStatusLineUpdate(e)}),await this.internalApi.start(0),this.internalApi.setMcpBridgeUpHandler(e=>this.onMcpBridgeUp(e)),this.internalApiPort=parseInt(new URL(this.internalApi.baseUrl).port,10),this.notifyPort=0,a.info("claude-adapter",`Adapter started (stdio MCP mode, internal API at ${this.internalApi.baseUrl})`)}onMcpBridgeUp(e){this.bridgeCallbacks.sendMcpFrame?.(e)}deliverMcpFrameToAgent(e){this.internalApi?.sendMcpFrameToBridge(e)}async stop(){if(a.info("claude-adapter",`Stopping adapter (sessionId=${this.sessionId}, alive=${this.alive}, activeEvent=${this.activeEvent?.eventId??"none"})`),this.lifecycleVersion+=1,this.stopped=!0,this.alive=!1,this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"canceled","adapter stopped"),this.clearActiveEvent()),this.stopComposing(),this.stopSelfDriven(),this.clearStopHookBarrier(),this.compacting=!1,this.compactingTimer&&(clearTimeout(this.compactingTimer),this.compactingTimer=null),this.compactionDoneResolver&&(this.compactionDoneResolver("stopped"),this.compactionDoneResolver=null),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null),this.pendingPermissions.size>0){const s=E(),n=new q(s.permissionRequestsDir);for(const[r]of this.pendingPermissions)n.resolveRequest(r,"deny").catch(()=>{});this.pendingPermissions.clear()}if(this.activityManager&&(this.activityManager.stop(),this.activityManager=null),this.sessionId){const s=this.resolveHookSignalsPath();se(s).catch(()=>{})}this.stopMcpServer();const e=this.claudeChildPid;this.claudeChildPid=0;const t=this.claudeProcess,i=this.claudePty;if(this.claudeProcess=null,this.claudePty=null,this.spawnPromise=null,i)try{i.kill()}catch{}if(e>0)try{process.kill(e,"SIGTERM")}catch{}if(t?.pid&&(N(t,"SIGTERM"),!await Promise.race([new Promise(n=>{t.once("exit",()=>n(!0))}),new Promise(n=>{setTimeout(()=>n(!1),5e3)})]))){if(e>0)try{process.kill(e,"SIGKILL")}catch{}N(t,"SIGKILL")}if(e>0){const s=Date.now();for(;Date.now()-s<5e3;)try{process.kill(e,0),await new Promise(n=>setTimeout(n,100))}catch{break}try{process.kill(e,"SIGKILL")}catch{}}this.releaseNotifyPortReservation(),this.claudeCliSessionId&&z(this.claudeCliSessionId),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.alive}async createSession(e){return`claude-session-${Date.now()}`}async resumeSession(e,t){}async destroySession(e){}sendPrompt(e){const t=new _t(e.adapterSessionId),i={event_id:e.adapterSessionId,session_id:e.adapterSessionId,content:e.text,context_messages_json:e.contextMessages?JSON.stringify(e.contextMessages):void 0};return this.deliverInboundEvent(i),this.once(`reply:${e.adapterSessionId}`,s=>{t.emitDone(s.status==="completed"?{status:"completed"}:{status:"failed",error:"failed"})}),t}async cancel(e){}setPermissionHandler(e){}async ping(e){if(this.stopped)return!1;if(!this.claudeProcess&&!this.claudePty)return this.alive&&this.internalApi!==null;if(!(this.alive&&this.mcpServerReady&&(this.startupChannelListening||process.platform==="win32")))return!1;const t=this.claudeProcess?.pid??this.claudePty?.pid;if(t&&!pt(t))return!1;const i=this.claudeProcess;if(i){const s=!!(i.stdin&&!i.stdin.destroyed),n=!!(i.stdout&&!i.stdout.destroyed);if(!s||!n)return!1}return!0}getStatus(){return{alive:this.alive,busy:this.activeEvent!==null||this.compacting,sessions:0,details:this.sessionState?{sessionState:this.sessionState}:void 0}}getActiveEventIds(){return this.activeEvent?[this.activeEvent.eventId]:[]}clearActiveEventForShutdown(){this.activeEvent&&this.clearActiveEvent()}getSessionState(){return this.sessionState}getMcpConfig(){return null}async hasBackgroundWork(){const e=this.claudeProcess?.pid??this.claudePty?.pid;if(!e)return!1;const t=[this.claudeProcess?.pid,this.claudeChildPid||void 0,this.claudePty?.pid].filter(i=>!!i&&i>0);return Ae(e,t)}async probe(e){const t=this.config.command||"claude",i=await Xe(t),s=i!==null;let n=null,r;if(s){const f=await Ke(t);n=f.version,f.error&&(r=f.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const l=(Ye().ANTHROPIC_BASE_URL??process.env.ANTHROPIC_BASE_URL??"").trim()||null,c=(this.sessionState?.model?.id??(process.env.ANTHROPIC_MODEL??"").trim())||null;let u={attempted:!1,ok:!1,latency_ms:null};if(e?.conversation&&s){const f=Date.now();try{await Ie(ke)(t,["-p","ping","--output-format","json","--max-turns","1"],{timeout:e.timeoutMs??8e3,encoding:"utf-8"}),u={attempted:!0,ok:!0,latency_ms:Date.now()-f}}catch(p){const S=p,k=S.killed?"conversation_timeout":"conversation_failed";u={attempted:!0,ok:!1,latency_ms:Date.now()-f,error:{code:k,message:S.message??String(p)}}}}return{cli:{command:t,installed:s,path:i,version:n,...r?{error:r}:{}},conversation:u,config:{model:c,base_url:l,source:{model:this.sessionState?.model?.id?"runtime":process.env.ANTHROPIC_MODEL?"env":"unknown",base_url:l?"env":"unknown"}},process:{started:!!(this.claudeProcess||this.claudePty),alive:this.alive,busy:this.activeEvent!==null},...this.probeSessionRecord()}}probeSessionRecord(){const e=this.claudeCliSessionId;if(!e||!this.claudeSessionCwd)return{session:{recordPath:null,lastActivityMs:null,freshMs:null}};const t=Y(e,this.claudeSessionCwd);try{const i=L(t);return{session:{recordPath:t,lastActivityMs:i.mtimeMs,freshMs:Date.now()-i.mtimeMs}}}catch{return{session:{recordPath:t,lastActivityMs:null,freshMs:null}}}}getSupportedCommands(){return[{name:"compact",description:"\u538B\u7F29\u4E0A\u4E0B\u6587",args:"[instructions]"},{name:"clear",description:"\u6E05\u9664\u5BF9\u8BDD"},{name:"model",description:"\u5207\u6362\u6A21\u578B",args:"<model-id>"},{name:"cost",description:"\u663E\u793A\u8D39\u7528"},{name:"rewind",description:"\u56DE\u9000\u5BF9\u8BDD"},{name:"memory",description:"\u8BB0\u5FC6\u7BA1\u7406"},{name:"doctor",description:"\u8BCA\u65AD"},{name:"status",description:"\u72B6\u6001\u663E\u793A"},{name:"skills",description:"\u83B7\u53D6 skills \u6E05\u5355"}]}async execCommand(e,t,i){if(e==="skills")try{const n=We({mode:"claude",projectDir:this.claudeSessionCwd}),r=n.map(o=>{const l=o.trigger?` (${o.trigger})`:"",c=o.pluginName?` [plugin:${o.pluginName}]`:` [${o.source}]`;return`- ${o.name}${l}${c}: ${o.description}`});return{status:"ok",message:r.length>0?r.join(`
`):"No skills found",data:n}}catch(n){return{status:"failed",message:`Failed to scan skills: ${n instanceof Error?n.message:n}`}}if(!this.claudeProcess&&!this.claudePty)if(e==="compact"){try{await this.ensureClaudeProcessReady()}catch(n){return{status:"failed",message:`Failed to start Claude for compact: ${n instanceof Error?n.message:n}`}}if(await new Promise(n=>setTimeout(n,2e3)),!this.claudeProcess&&!this.claudePty)return{status:"failed",message:"Claude process exited during startup"}}else return{status:"failed",message:"Claude process is not running"};if(this.getStatus().busy)return{status:"failed",message:"Claude is busy, try again later"};const s=t?`/${e} ${t}`:`/${e}`;try{if(this.claudePty)this.claudePty.write(`${s}\r`);else if(this.expectRunDir){const n=h(this.expectRunDir,"cmd.fifo");let r;try{r=Re(n,ie.O_WRONLY|ie.O_NONBLOCK)}catch(o){const l=o.code;throw new Error(l==="ENXIO"?"expect process is not running \u2014 cannot inject command":`FIFO open failed: ${o instanceof Error?o.message:o}`)}try{De(r,`${s}
`)}finally{Me(r)}}else this.claudeProcess?.stdin?.write(`${s}
`);if(e==="compact"){const n=new Promise(o=>{this.compactionDoneResolver=o});this.beginCompaction();const r=await n;return r==="process-exit"?{status:"failed",message:"Claude exited during compaction"}:r==="stopped"?{status:"failed",message:"Adapter stopped during compaction"}:{status:"ok",message:"Compacted"}}return{status:"ok",message:`Sent: ${s}`}}catch(n){return{status:"failed",message:`Failed to send: ${n instanceof Error?n.message:n}`}}}handleStatusLineUpdate(e){try{const t=e.context_window,i=e.cost,s=e.model,n=e.rate_limits??e.rateLimits,r=t?.current_usage;if(this.sessionState={contextWindow:{usedPercentage:t?.used_percentage!=null?y(t.used_percentage):null,remainingPercentage:t?.remaining_percentage!=null?y(t.remaining_percentage):null,totalInputTokens:y(t?.total_input_tokens),totalOutputTokens:y(t?.total_output_tokens),contextWindowSize:y(t?.context_window_size)||2e5,currentUsage:r?{inputTokens:y(r.input_tokens),outputTokens:y(r.output_tokens),cacheCreationInputTokens:y(r.cache_creation_input_tokens),cacheReadInputTokens:y(r.cache_read_input_tokens)}:null},cost:{totalCostUsd:y(i?.total_cost_usd),totalDurationMs:y(i?.total_duration_ms),totalApiDurationMs:y(i?.total_api_duration_ms),totalLinesAdded:y(i?.total_lines_added),totalLinesRemoved:y(i?.total_lines_removed)},rateLimits:this.parseStatusRateLimits(n),model:{id:String(s?.id??""),displayName:String(s?.display_name??"")},fastMode:e.fast_mode===!0,effort:e.effort!=null?String(e.effort):void 0,thinkingEnabled:e.thinking_enabled===!0,exceeds200kTokens:e.exceeds_200k_tokens===!0,version:String(e.version??""),sampledAt:Date.now()},n){const o=this.sessionState.rateLimits?.fiveHour,l=this.sessionState.rateLimits?.sevenDay;a.info("claude-adapter",`[rate-limits] statusLine parsed: fiveHour=${o?`${o.usedPercentage}% resetsAt=${o.resetsAt}`:"n/a"} sevenDay=${l?`${l.usedPercentage}% resetsAt=${l.resetsAt}`:"n/a"}`)}else a.debug("claude-adapter","[rate-limits] statusLine: no rate_limits in payload");try{this.bridgeCallbacks.onStatusLineUpdated?.(this.sessionState)}catch(o){a.warn("claude-adapter",`onStatusLineUpdated callback error: ${o instanceof Error?o.message:o}`)}}catch(t){a.warn("claude-adapter",`Failed to parse statusLine payload: ${t instanceof Error?t.message:t}`)}}parseStatusRateLimits(e){if(!e)return;const t=n=>{if(!n||typeof n!="object")return;const r=n,o=y(r.used_percentage??r.usedPercent),l=y(r.resets_at??r.resetsAt);if(!(o<=0&&l<=0))return{usedPercentage:o,resetsAt:l}},i=t(e.five_hour??e.fiveHour),s=t(e.seven_day??e.sevenDay);if(!(!i&&!s))return{...i?{fiveHour:i}:{},...s?{sevenDay:s}:{}}}deliverInboundEvent(e){if(this.sessionId||(this.sessionId=e.session_id),this.pruneCompletedEvents(),this.completedEventIds.has(e.event_id)){a.info("claude-adapter",`Event ${e.event_id} rejected: duplicate`),this.bridgeCallbacks.sendEventResult(e.event_id,"responded","duplicate event");return}if(Date.now()<this.authFailureUntil){a.info("claude-adapter",`Event ${e.event_id} rejected: auth cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude authentication failed \u2014 please re-login");return}if(Date.now()<this.usageLimitUntil){a.info("claude-adapter",`Event ${e.event_id} rejected: usage limit cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude usage limit reached \u2014 waiting for reset");return}if(this.activeEvent?.eventId===e.event_id){a.info("claude-adapter",`Event ${e.event_id} ignored: same as active event`);return}if(this.activeEvent||this.compacting||this.stopHookBarrierSessionId===e.session_id){a.error("claude-adapter",`Scheduler invariant violated: event ${e.event_id} delivered while busy (active=${this.activeEvent?.eventId??"none"}, compacting=${this.compacting}, barrier=${this.stopHookBarrierSessionId===e.session_id}) \u2014 failing event to release slot`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent busy (scheduler invariant violated)");return}this.activeEvent={eventId:e.event_id,sessionId:e.session_id,rawEvent:e},this.stopSelfDriven(),this.lastClearedEvent=null,this.markActiveEventActivity(e.event_id,e.session_id),this.resetActiveEventHardTimer(e.event_id),this.emit("eventStarted",e.event_id,e.session_id),this.ensureClaudeAndPushEvent(e)}deliverStopEvent(e,t){if(this.activeEvent?.eventId!==e){a.info("claude-adapter",`Stop for non-active event=${e} (active=${this.activeEvent?.eventId??"none"}, compacting=${this.compacting}) \u2014 emitting eventDone to unblock platform`),this.emit("eventDone",e);return}const i=this.activeEvent.sessionId;a.info("claude-adapter",`Stop requested for active event=${e} \u2014 killing Claude process`),this.mcpServerReady&&this.pushNotification("notifications/event_stop",{event_id:e,session_id:i,stop_id:H()}),(this.claudeProcess||this.claudePty)&&this.emit("pauseIntake","restart"),this.bridgeCallbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActiveEvent(),this.killClaudeProcess("stop")}async handleLocalAction(e){if(!this.mcpServerReady)return{handled:!1,kind:""};const t=String(e.action_type??"");if(t==="claude_interaction_reply"){const i=e.params??{};if(String(i.kind??"")==="permission"){const n=i.resolution??{},r=String(n.value??""),o={...e,action_type:r==="allow"?"exec_approve":"exec_reject",params:{...i,approval_command_id:i.request_id}};return this.handlePermissionApproval(o)}return this.handleQuestionReply(e)}return t==="exec_approve"||t==="exec_reject"?this.handlePermissionApproval(e):(this.pushNotification("notifications/local_action",e),{handled:!0,kind:""})}async handleQuestionReply(e){const t=e.params??{},i=String(t.request_id??"");if(!i)return a.warn("claude-adapter","Question reply missing request_id"),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"request_id_required","request_id is required"),{handled:!0,kind:"question_reply_no_id"};const s=E(),n=new ae(s.questionRequestsDir),r=t.resolution??{},o=String(r.type??""),l=r;if(o==="action"&&String(r.value??"")==="cancel"){a.info("claude-adapter",`Question cancelled by user: request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,l);const p=this.pendingQuestion.get(i);return p&&this.activeEvent?.eventId===p.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"cancel"}),{handled:!0,kind:"question_reply_cancel"}}let c="";if(o==="text"?c=String(r.value??""):o==="map"&&(c=(Array.isArray(r.entries)?r.entries:[]).map(p=>p.value).join(", ")),!c){a.warn("claude-adapter",`Empty answer for question reply request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,l);const f=this.pendingQuestion.get(i);return f&&this.activeEvent?.eventId===f.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"cancel"}),{handled:!0,kind:"question_reply_empty"}}a.info("claude-adapter",`Resolving question in store: request_id=${i} answer=${c.slice(0,80)}`),await n.resolveRequest(i,"answer",c,l);const u=this.pendingQuestion.get(i);return u&&this.activeEvent?.eventId===u.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:i,resolution:"answer"}),{handled:!0,kind:"question_reply"}}async handlePermissionApproval(e){const t=e.params??{},i=String(t.approval_id??t.approval_command_id??t.tool_call_id??""),s=String(e.action_type??"")==="exec_approve";if(!i)return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_id_required","approval_id is required"),{handled:!0,kind:"permission_approval"};if(!this.pendingPermissions.get(i))return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_not_found",`no pending permission for approval_id: ${i}`),{handled:!0,kind:"permission_approval"};const r=E(),o=new q(r.permissionRequestsDir),l=s?"allow":"deny";return await o.resolveRequest(i,l),this.pendingPermissions.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok"),a.info("claude-adapter",`Permission ${l}: approvalId=${i}`),{handled:!0,kind:"permission_approval"}}async ensureClaudeAndPushEvent(e,t=0){t===0&&!this.claudeProcess&&this.startComposing(e.session_id,"preparing");try{if(this.mcpChannelBroken){await this.runSingleTurnFallback(e,"mcp channel unavailable");return}if(await this.isChannelGateClosed()){await this.runSingleTurnFallback(e,"channel gate closed: tengu_harbor not enabled in cachedGrowthBookFeatures");return}if(await this.ensureClaudeProcessReady(),!this.mcpServerReady)throw new Error("MCP stdio server not available");if(await this.waitForNotifyPort(3e4),process.platform==="win32"?await this.waitForWindowsChannelReady(ve):await this.waitForChannelListening(ve),this.activeEvent?.eventId!==e.event_id||this.mcpChannelBroken)return;const s={},n=[["chat_id",e.session_id],["event_id",e.event_id],["event_type","user_chat"],["message_id",e.msg_id],["sender_id",e.sender_id],["user",e.sender_id],["user_id",e.sender_id],["msg_id",e.msg_id],["session_type",e.session_type!=null?String(e.session_type):void 0],["msg_type",e.msg_type!=null?String(e.msg_type):void 0],["quoted_message_id",e.quoted_message_id],["ts",e.created_at!=null?new Date(Number(e.created_at)).toISOString():void 0],["owner_id",e.owner_id],["agent_id",e.agent_id],["attachments_json",e.attachments_json],["context_messages_json",e.context_messages_json]];for(const[r,o]of n)o!=null&&o!==""&&(s[r]=o);this.captureEventJsonlBaseOffset(e.event_id),this.pushNotification("notifications/claude/channel",{content:e.content??"",meta:s}),a.info("claude-adapter",`Event ${e.event_id} pushed to MCP stdio server`)}catch(s){if(this.stopped){this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"canceled","adapter stopped"),this.clearActiveEvent());return}if(s instanceof Error&&(s.message.includes("exited before")||s.message.includes("timeout")||s.message.includes("spawn failed"))&&t<3)return a.warn("claude-adapter",`Spawn failed (attempt ${t+1}/4), retrying: ${s}`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),z(this.claudeCliSessionId),ee(this.claudeCliSessionId,[]),await new Promise(r=>setTimeout(r,(t+1)*2e3)),this.ensureClaudeAndPushEvent(e,t+1);if(a.error("claude-adapter",`Failed to deliver event ${e.event_id}: ${s}`),this.shouldFallbackToSingleTurn(s)){a.warn("claude-adapter",`Channel path failed, switching to single-turn fallback: ${s instanceof Error?s.message:String(s)}`);try{await this.runSingleTurnFallback(e,s instanceof Error?s.message:String(s));return}catch(r){a.error("claude-adapter",`Single-turn fallback failed for ${e.event_id}: ${r instanceof Error?r.message:String(r)}`)}}this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"failed",s instanceof Error?s.message:String(s)),this.clearActiveEvent())}}shouldFallbackToSingleTurn(e){const t=(e instanceof Error?e.message:String(e)).toLowerCase();return t?t.includes("mcp server startup failed")||t.includes("claude channel listener not ready")||t.includes("channel ready check timed out")||t.includes("channels are not currently available")||t.includes("mcp stdio server not available")||t.includes("notify port not ready")||t.includes("file not found:"):!1}buildSingleTurnPrompt(e,t=!0){const i=t&&e.context_messages_json?String(e.context_messages_json):"",s=["Reply to the latest user message directly and concisely."];return i&&(s.push("Conversation context (JSON):"),s.push(i)),s.push("Latest user message:"),s.push(e.content??""),s.join(`
`)}extractSingleTurnDelta(e){const t=e.trim();if(!t)return"";try{const i=JSON.parse(t),s=String(i.type??"");if(s==="content_block_delta"){const n=i.delta;if(n&&typeof n=="object"){const r=String(n.text??"");if(r)return r}}return s==="result"?String(i.result??""):typeof i.text=="string"&&i.text?i.text:""}catch{return t}}getCliVersion(){if(this.cachedCliVersion)return this.cachedCliVersion;const e=this.config.command||"claude";try{const i=j(`${e} --version`,{encoding:"utf-8",timeout:5e3}).trim().match(/^([\d.]+)/);this.cachedCliVersion=i?.[1]??"0.0.0"}catch{this.cachedCliVersion="0.0.0"}return this.cachedCliVersion}static versionGte(e,t){const i=e.split(".").map(Number),s=t.split(".").map(Number);for(let n=0;n<Math.max(i.length,s.length);n++){const r=i[n]??0,o=s[n]??0;if(r>o)return!0;if(r<o)return!1}return!0}buildClaudeRuntimeEnv(){const e={...process.env,...this.config.env};return delete e.CLAUDECODE,e.CLAUDE_PLUGIN_DATA=E().dataDir,e.GRIX_HOOK_SIGNALS_PATH=this.resolveHookSignalsPath(),this.internalApi&&(e.GRIX_CLAUDE_INTERNAL_API_URL=this.internalApi.baseUrl,e.GRIX_CLAUDE_INTERNAL_API_TOKEN=this.internalApi.token),e}async runSingleTurnFallback(e,t){const i=this.resolveSessionRuntime(),s=this.config.options??{},n=this.config.command||"claude",r=(this.config.args??[]).filter(b=>{const _=String(b).trim().toLowerCase();return _!=="--dangerously-load-development-channels"&&_!=="--session-id"&&_!=="--resume"}),o=String(i.cwd??"").trim(),l=String(i.modelId??"").trim(),c=Se(i.modeId);await this.ensureWorkspaceTrust(o),await this.ensureClaudeOnboardingFlags(o),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(o),await this.ensureStdioMcpServer();const u=this.buildClaudeRuntimeEnv(),f=i.pluginDir??s.pluginDir??await this.ensureClaudePluginDir(),p=String(i.claudeSessionId??"").trim()||this.claudeCliSessionId||H();this.claudeCliSessionId=p,this.claudeSessionCwd=o,!i.claudeSessionId&&i.onSessionIdAssigned&&i.onSessionIdAssigned(p);const S=Ee(p,o||void 0),k=this.buildSingleTurnPrompt(e,!S),v=[...r,"-p","--output-format","stream-json","--include-partial-messages",...S?["--resume",p]:["--session-id",p]];we.versionGte(this.getCliVersion(),"2.1.150")&&v.push("--verbose"),v.push("--tools","","--plugin-dir",f,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath,"--permission-mode",c===F.fullAuto?"bypassPermissions":"default"),l&&v.push("--model",l),v.push(k),a.warn("claude-adapter",`Running single-turn fallback for event ${e.event_id}: ${t}`),this.bridgeCallbacks.sendEventAck(e.event_id,e.session_id),this.startComposing(e.session_id,"fallback_single_turn"),await new Promise((b,_)=>{const T=W(n,v,{cwd:o||process.cwd(),env:u,stdio:["ignore","pipe","pipe"],detached:!0,windowsHide:!0});let g="",m=0,P=!1;const I=`fallback_${e.event_id}_${Date.now()}`;let $="";T.stdout?.setEncoding("utf8"),T.stdout?.on("data",R=>{$+=R;let A=$.indexOf(`
`);for(;A>=0;){const Pe=$.slice(0,A);$=$.slice(A+1);const te=this.extractSingleTurnDelta(Pe);te&&(m+=1,P=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,te,m,!1,I,e.quoted_message_id)),A=$.indexOf(`
`)}}),T.stderr?.setEncoding("utf8"),T.stderr?.on("data",R=>{g.length>=K||(g+=R,g.length>K&&(g=g.slice(0,K)))}),T.on("error",R=>_(R)),T.on("close",R=>{if($.trim()){const A=this.extractSingleTurnDelta($);A&&(m+=1,P=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,A,m,!1,I,e.quoted_message_id))}if(R!==0&&!P){const A=g.trim();_(new Error(A||`claude single-turn exited with code ${R}`));return}m+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",m,!0,I,e.quoted_message_id),b()})}),this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"responded"),this.clearActiveEvent())}async ensureClaudeProcessReady(){this.claudeProcess||this.claudePty||(this.spawnPromise||(this.spawnPromise=this.spawnClaude().finally(()=>{this.spawnPromise=null})),await this.spawnPromise)}async spawnClaude(){if(this.claudeProcess)return;const e=this.lifecycleVersion,t=()=>{if(this.stopped||e!==this.lifecycleVersion)throw new Error("adapter stopped")},i=this.config.options??{};this.sessionIdConflictDetected=!1;const s=this.resolveSessionRuntime(),n=this.config.command||"claude",r=this.config.args??[],o=this.buildClaudeRuntimeEnv(),l=Se(s.modeId),c=String(s.modelId??"").trim(),u=String(s.cwd??"").trim();this.claudeSessionCwd=u;const f=String(s.claudeSessionId??"").trim()||H();this.claudeCliSessionId=f,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(f);const p=Ee(f,u||void 0);if(p||(ee(f,[]),z(f)),this.runtimeResolver&&!u)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!u)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(u),await this.ensureClaudeOnboardingFlags(u),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(u);let S=!1,k="",v=null;try{t(),this.notifyPort=await yt([this.internalApiPort]),S=!0,t(),a.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const _=i.pluginDir||await this.ensureClaudePluginDir(),T=p?["--resume",f]:["--session-id",f],g=[...r,"--name",`grix-${this.sessionId}`,...T,"--plugin-dir",_,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath];if(c&&(/^claude/i.test(c)?g.push("--model",c):(a.warn("claude-adapter",`Skipping --model ${c}: non-Anthropic models are incompatible with development channels. Using default model so channels can load. Switch models after startup via /model if needed.`),this.deferredModelId=c)),l===F.fullAuto&&g.push("--dangerously-skip-permissions"),g.push("--dangerously-load-development-channels",`server:${O}`),process.platform==="win32"){const m=ye.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),P=kt(this.claudeCliSessionId);await At(P,m),g.push("--append-system-prompt-file",P)}else g.push("--append-system-prompt",ye);if(process.platform==="win32")if(U){const m=bt(),P=["/c",n,...g];a.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${u} mode=${l} ${p?"resume":"new"} ${n} ${g.join(" ")}`);const I=U.spawn(m,P,{name:"xterm-256color",cols:120,rows:30,cwd:u,env:o,useConpty:!0,conptyInheritCursor:!1});this.claudePty=I,this.claudeChildPid=I.pid,this.startPtyAutoConfirm(I),v=null}else a.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${u} mode=${l} ${p?"resume":"new"} ${n} ${g.join(" ")}`),v=W(n,g,{cwd:u,env:o,stdio:["pipe","pipe","pipe"],detached:!0,shell:!0,windowsHide:!0});else{a.info("claude-adapter",`Spawning via expect PTY: cwd=${u} mode=${l} ${p?"resume":"new"} ${n} ${g.join(" ")}`);try{L("/usr/bin/expect")}catch{throw new Error("/usr/bin/expect not found. Install it with: apt install expect / dnf install expect / brew install expect")}const m=h(Oe(),`grix-claude-${H()}`);await x(m,{recursive:!0}),this.expectRunDir=m;const{expectPath:P,pidPath:I}=await Et(m,n,g);k=I,t(),v=W("/usr/bin/expect",[P],{cwd:u,env:o,stdio:["pipe","pipe","pipe"],detached:!0})}if(this.claudeProcess=v,this.clearPendingMcpFailureTimer(),this.mcpStartupFailureHandled=!1,this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,v&&v.on("error",m=>{a.error("claude-adapter","Claude process spawn error: "+m),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude process spawn failed"),this.clearActiveEvent())}),t(),!this.claudePty)if(process.platform==="win32")this.claudeChildPid=v?.pid??0;else{const m=await Pt(v,k);if(t(),!Number.isFinite(m)||m<=0)throw new Error("failed to determine spawned Claude pid");this.claudeChildPid=m}if(!this.claudeChildPid||this.claudeChildPid<=0)throw new Error("failed to determine spawned Claude pid")}catch(_){if(v?.pid&&N(v,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===v&&(this.claudeProcess=null),this.claudeChildPid=0,S&&this.releaseNotifyPortReservation(),this.stopMcpServer(),_}const b=this.claudeChildPid;a.info("claude-adapter","Claude child PID: "+b+(this.claudePty?" (via PTY)":"")),this.setupProcessOrPtyHandlers(),this.alive=!0}setupProcessOrPtyHandlers(){this.claudeProcess&&(this.claudeProcess.on("exit",(e,t)=>{if(a.info("claude-adapter",`Claude process exited (code=${e}, signal=${t})`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const i=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";a.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${i}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,i,s),this.clearActiveEvent()}this.stopped||this.emit("exit",e),this.compacting&&this.finishCompaction("process-exit")}}),this.claudeProcess.stdout?.on("data",e=>{this.handleClaudeOutput(e.toString())}),this.claudeProcess.stderr?.on("data",e=>{const t=e.toString().trim();t&&a.info("claude-adapter",`[claude stderr] ${t}`),this.checkFailurePatterns(t)})),this.claudePty&&(this.claudePty.onExit(({exitCode:e,signal:t})=>{if(a.info("claude-adapter",`Claude PTY process exited (code=${e}, signal=${t})`),this.claudePty=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const i=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";a.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${i}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,i,s),this.clearActiveEvent()}this.stopped||this.emit("exit",e),this.compacting&&this.finishCompaction("process-exit")}}),this.claudePty.onData(e=>{this.handleClaudeOutput(e),this.checkFailurePatterns(e)}))}lastPtyOutputAt=0;ptyProbe={title:0,spinner:0,meaningful:0,lastLogAt:0,lastMeaningfulAt:0};recordPtyProbe(e){const t=Date.now();this.lastPtyOutputAt=t;const i=e.includes("]0;"),s=e.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f]/g,"").trim(),n=s.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(s);if(i?this.ptyProbe.title++:!s||n?this.ptyProbe.spinner++:(this.ptyProbe.meaningful++,this.ptyProbe.lastMeaningfulAt=t),t-this.ptyProbe.lastLogAt>=2e3){const r=this.activeEvent?this.activeEvent.replied?"replied":"running":"none",o=this.ptyProbe.lastMeaningfulAt?t-this.ptyProbe.lastMeaningfulAt:-1;a.debug("claude-adapter",`[pty-probe] session=${this.sessionId??"?"} event=${r} win2s title=${this.ptyProbe.title} spinner=${this.ptyProbe.spinner} meaningful=${this.ptyProbe.meaningful} meaningfulGapMs=${o}`),this.ptyProbe.title=0,this.ptyProbe.spinner=0,this.ptyProbe.meaningful=0,this.ptyProbe.lastLogAt=t}}handleClaudeOutput(e){this.recordPtyProbe(e),this.compacting&&this.resetCompactingActivityTimer();const t=e.trim();if(t){const o=t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202f\ufeff]/g,"").trim();o?o.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(o)?a.debug("claude-adapter",`[claude output] ${o}`):a.info("claude-adapter",`[claude output] ${o.slice(0,500)}`):a.debug("claude-adapter",`[claude output] ${t.slice(0,200).replace(/[^\x20-\x7e]/g,l=>`\\x${l.charCodeAt(0).toString(16).padStart(2,"0")}`)}`)}this.activeEvent&&this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId);const i=t.match(/Session ID (\S+) is already in use/i);if(i){const o=i[1];this.sessionIdConflictDetected=!0;const l=[this.claudeProcess?.pid,this.claudeChildPid,this.claudePty?.pid].filter(c=>!!c&&c>0);ee(o,l)}const s=t.replace(/\[[0-9;?]*[ -/]*[@-~]/g," ").replace(/[^a-zA-Z]+/g," ").toLowerCase(),n=s.replace(/ /g,"");if(this.claudePty&&!this.startupChannelListening&&/trust.*folder|quick.*safety.*check/.test(n)&&(a.info("claude-adapter","Auto-accepting workspace trust dialog"),this.claudePty.write("1\r")),(/listeningforch\w*nelmessages/.test(n)||/nnelmessagesfrom/.test(n)||/inboundmessageswillbepushedintothissession/.test(n))&&(this.startupChannelListening||(this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.sendDeferredModelSwitch()),this.clearPendingMcpFailureTimer()),!this.startupChannelListening&&/channels?arenotcurrentlyavailable/.test(n)&&(this.channelGateClosed=!0,a.warn("claude-adapter",'Claude reports "Channels are not currently available" \u2014 channel gate (tengu_harbor) is closed for this account/model; switching to single-turn fallback')),!this.mcpStartupFailureHandled&&s.includes("mcp server failed")){if(this.startupChannelListening){a.warn("claude-adapter","Claude reported MCP server failed for one channel, but channel listening is active; ignoring non-blocking failure");return}this.pendingMcpFailureTimer||(this.pendingMcpFailureTimer=setTimeout(()=>{this.pendingMcpFailureTimer=null,!(this.startupChannelListening||this.mcpStartupFailureHandled)&&this.markMcpStartupFailure()},vt))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),tt.some(t=>t.test(e))&&(a.error("claude-adapter",`Auth failure: ${e}`),this.authFailureUntil=Date.now()+de,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
Error: Claude authentication failed -- please re-login`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude authentication failed -- please re-login"),this.clearActiveEvent())),it.some(t=>t.test(e))&&(a.error("claude-adapter",`Usage limit: ${e}`),this.usageLimitUntil=Date.now()+de,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
Error: Claude usage limit reached -- waiting for reset`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude usage limit reached -- waiting for reset"),this.clearActiveEvent())),st.test(e)&&this.activeEvent&&(a.error("claude-adapter",`API format error (400 server_tool_use.id): ${e.slice(0,200)}`),this.activeEvent.apiFormatError=!0)}async ensureClaudePluginDir(){const e=this.resolveProjectRoot(),t=h(E().dataDir,"claude-plugin"),i=h(t,".claude-plugin"),s=h(i,"plugin.json"),n=h(e,".claude-plugin","plugin.json");await x(i,{recursive:!0});let r="";try{r=await w(n,"utf8")}catch{r=`${JSON.stringify({name:"grix-connector",version:"0.1.0",description:"Claude Code channel plugin for grix-connector.",license:"MIT"},null,2)}
`}let o="";try{o=await w(s,"utf8")}catch{}o!==r&&(await C(s,r,"utf8"),a.info("claude-adapter",`Wrote Claude plugin manifest: ${s}`));const l=h(e,"dist","scripts"),c=_=>`"${String(_).replace(/"/g,'\\"')}"`,u=_=>`${c(process.execPath)} ${c(h(l,_))}`,f=`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],Elicitation:[{hooks:[{type:"command",command:u("elicitation-hook.js")}]}],ElicitationResult:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],UserPromptSubmit:[{hooks:[{type:"command",command:u("user-prompt-submit-hook.js")}]}],PreToolUse:[{matcher:"ExitPlanMode",hooks:[{type:"command",command:u("approve-plan-hook.js")}]},{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PostToolUse:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PostToolUseFailure:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PermissionRequest:[{matcher:"",hooks:[{type:"command",command:u("permission-hook.js")}]}],Notification:[{matcher:"idle_prompt",hooks:[{type:"command",command:u("notification-hook.js")}]}],PermissionDenied:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],Stop:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],StopFailure:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PreCompact:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PostCompact:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],ConfigChange:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}]}},null,2)}
`,p=h(t,"hooks"),S=h(p,"hooks.json");await x(p,{recursive:!0});let k="";try{k=await w(S,"utf8")}catch{}k!==f&&(await C(S,f,"utf8"),a.info("claude-adapter",`Wrote Claude hooks config: ${S}`));const v=h(t,"skills"),b=Qe(v);return b.length>0&&a.info("claude-adapter",`Synced connector skills to plugin: [${b.join(", ")}]`),t}async ensureStdioMcpServer(){const e=this.resolveProjectRoot(),t=this.resolveStdioServerPath(e);if(this.ensureStdioServerArtifact(e,t),!D(t))throw new Error(`MCP stdio server entry point not found: ${t}`);const i=this.getInternalApiUrl(),s=this.notifyPort,n=[t,"--handle-url",i,"--notify-port",String(s)],r=h(e,"dist","mcp","mcp-bridge-server.js"),o=this.internalApi?.mcpBridgeWsUrl,l=D(r)&&o?[r,"--ws-url",o]:void 0;l||a.warn("claude-adapter",`APP MCP bridge skipped (path=${D(r)} ws=${!!o})`);const c=Ct(this.claudeCliSessionId);await It(c,n,l),this.claudeMcpConfigPath=c,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=Ne(import.meta.url);return Le(e,"..","..","..","..")}resolveStdioServerPath(e=this.resolveProjectRoot()){return h(e,"dist","mcp","stdio","server.js")}ensureStdioServerArtifact(e,t){if(D(t)||ge)return;ge=!0;const i=h(e,"node_modules","typescript","bin","tsc"),s=h(e,"tsconfig.json");if(!D(i)||!D(s))return;a.warn("claude-adapter",`MCP stdio server artifact missing, attempting build: ${t}`);const n=Ce(process.execPath,[i,"-p",s],{cwd:e,env:process.env,encoding:"utf8",timeout:6e4});if(n.status!==0){const r=`${n.stderr??""}${n.stdout??""}`.trim();throw new Error(`MCP stdio server build failed: ${r||`exit ${n.status??"unknown"}`}`)}}getInternalApiUrl(){return this.internalApi?this.internalApi.url:process.env.GRIX_CONNECTOR_INTERNAL_API?process.env.GRIX_CONNECTOR_INTERNAL_API:`http://127.0.0.1:${this.internalApiPort}`}internalApiPort=0;notifyPort=0;notifySocket=null;async waitForNotifyPort(e){if(this.notifyPort<=0)return;const t=Date.now();for(;Date.now()-t<e;)try{await new Promise((i,s)=>{const n=V.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{n.destroy(),i()});n.on("error",s),setTimeout(()=>{n.destroy(),s(new Error("probe timeout"))},2e3)});return}catch{await new Promise(i=>setTimeout(i,500))}throw new Error(`Notify port ${this.notifyPort} not ready within ${e}ms`)}async waitForChannelListening(e){const t=Date.now(),i=8e3;for(;Date.now()-t<e;){if(this.channelGateClosed)throw new Error("channels are not currently available (tengu_harbor gate closed)");if(this.startupChannelListening){const s=this.startupChannelListeningAt||Date.now(),n=Date.now()-s;if(n>=G)return;await new Promise(r=>setTimeout(r,G-n));return}if(this.alive&&this.notifyPort>0&&Date.now()-t>i){a.info("claude-adapter",`Channel listener fallback: notify port connected, assuming ready after ${Date.now()-t}ms (resume mode may skip "Listening" output)`),this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.clearPendingMcpFailureTimer(),this.sendDeferredModelSwitch();return}await new Promise(s=>setTimeout(s,200))}throw new Error(`Claude channel listener not ready within ${e}ms`)}ptyAutoConfirmTimer=null;startPtyAutoConfirm(e){this.ptyAutoConfirmTimer&&clearInterval(this.ptyAutoConfirmTimer);let t=!1;const i=()=>{if(this.startupChannelListening||this.stopped){this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null);return}try{e.write("\r"),t||(a.info("claude-adapter","PTY auto-confirm: sending Enter for dev channels dialog"),t=!0)}catch{}};setTimeout(i,1e3),this.ptyAutoConfirmTimer=setInterval(i,2e3),setTimeout(()=>{this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null)},3e4).unref()}sendDeferredModelSwitch(){if(!this.deferredModelId)return;const e=this.deferredModelId;this.deferredModelId=null,setTimeout(()=>{const t=this.claudePty??this.claudeProcess?.stdin;if(!(!t||this.stopped))try{const i=`/model ${e}
`;"write"in t?t.write(i):t.write(i,()=>{}),a.info("claude-adapter",`Deferred model switch: /model ${e}`)}catch{}},3e3)}async waitForWindowsChannelReady(e){const t=Date.now();let i=!1;const s=setInterval(()=>{if(this.startupChannelListening){clearInterval(s);return}try{this.claudePty?(this.claudePty.write("\r"),i||(a.info("claude-adapter","Windows PTY: sending Enter to auto-confirm dev channels dialog"),i=!0)):this.claudeProcess?.stdin?.writable&&(this.claudeProcess.stdin.write("\r"),i||(a.info("claude-adapter","Windows shell: sending Enter to auto-confirm dev channels dialog"),i=!0))}catch{}},3e3);try{for(;Date.now()-t<e;){if(this.channelGateClosed)throw new Error("channels are not currently available (tengu_harbor gate closed)");if(this.startupChannelListening){const o=this.startupChannelListeningAt||Date.now(),l=Date.now()-o;if(l>=G)return;await new Promise(c=>setTimeout(c,G-l));return}const n=Date.now()-t,r=this.claudePty?gt:12e3;if(this.alive&&this.mcpServerReady&&n>r){a.info("claude-adapter",`Windows ${this.claudePty?"PTY":"shell"} fallback: assuming