grix-connector 2.2.6 → 2.2.7
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.
- package/dist/adapter/claude/claude-adapter.js +12 -12
- package/dist/adapter/claude/claude-bridge-server.js +1 -1
- package/dist/adapter/claude/claude-tools.js +1 -1
- package/dist/adapter/claude/claude-worker-client.js +1 -1
- package/dist/adapter/claude/mcp-http-launcher.js +2 -2
- package/dist/adapter/claude/result-timeout.js +1 -1
- package/dist/core/file-ops/list-files.js +1 -1
- package/dist/core/mcp/tools.js +1 -1
- package/dist/log.js +2 -2
- package/dist/mcp/stdio/server.js +8 -8
- package/dist/mcp/stream-http/config.js +1 -1
- package/dist/mcp/stream-http/connection-binding.js +1 -1
- package/dist/mcp/stream-http/security.js +1 -1
- package/dist/mcp/stream-http/tool-executor.js +1 -1
- package/dist/mcp/stream-http/tool-registry.js +1 -1
- package/dist/mcp/stream-http/tool-schemas.js +1 -1
- package/openclaw-plugin/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import{spawn as W,spawnSync as Ce,execSync as j,execFile as ke}from"node:child_process";import{promisify as Ae}from"node:util";import{randomUUID as H}from"node:crypto";import{killProcessGroup as O,hasChildProcesses as Ie}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 xe,constants as ie,watch as Me}from"node:fs";import{mkdir as M,readFile as E,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 x,tmpdir as Fe}from"node:os";import V from"node:net";import{EventEmitter as re}from"node:events";import{fileURLToPath as Oe}from"node:url";let U=null;if(process.platform==="win32")try{U=await import("node-pty")}catch{U=null}import{resolveRuntimePaths as _}from"../../core/config/index.js";import{SESSION_MODE_IDS as N}from"./protocol-contract.js";import{log as a}from"../../core/log/index.js";import{ActivityStatusManager as Ne}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 Je,isGrixInternalToolName as Ge,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,extractTextBlocksAfterOffset as Ye,hasTerminalToolResultAfterOffset as oe,probeSessionTurnState as le,resolveSessionJsonlPath as Y}from"./usage-parser.js";import{readSettingsEnv as Xe}from"./model-list.js";import{resolveCliPath as Ke,getCliVersion as Ze}from"../../core/util/cli-probe.js";const F="grix";function et(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=et(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 tt(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 it=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],st=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],nt=/API Error:\s*400.*server_tool_use\.id/,de=300*1e3,rt=1800*1e3,ue=60*1e3,he=600*1e3,at=300*1e3,ot="The model's tool call could not be parsed (retry also failed).",lt="[\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",ct="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.",dt=2*1e3,X=90*1e3,pe=1800*1e3,fe=15*1e3,ut=300*1e3,me=10*1e3,ht=30*1e3,pt=30*1e3;function ft(d){try{return process.kill(d,0),!0}catch(e){return e.code==="EPERM"}}const mt=12e4,vt=3e4,gt=3e3,J=1e3,ve=45e3,yt=18e3,K=8192;let ge=!1;const Z=new Set;async function wt(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.","Your normal text output is delivered to the user automatically \u2014 just write your reply as plain text and it will be sent to the chat. You do NOT need a tool to talk to the user.","Use the reply tool ONLY when you must quote a specific earlier message (its reply_to). Never use the reply tool to resend text you have already written as plain text \u2014 that would deliver it twice.","If you intentionally do not want to send any visible message, 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 state the actual result as plain text. During a long-running task, write a short plain-text progress note when you reach a meaningful milestone so the user can see progress as it happens.',"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 we(d){return String(d??"").trim().toLowerCase()===N.approval?N.approval:N.fullAuto}class Ee 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;textForwardSeq=0;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=_(),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&&(O(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{}O(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&&!ft(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 Ie(e,t)}async probe(e){const t=this.config.command||"claude",i=await Ke(t),s=i!==null;let n=null,r;if(s){const f=await Ze(t);n=f.version,f.error&&(r=f.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const l=(Xe().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 Ae(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 w=p,k=w.killed?"conversation_timeout":"conversation_failed";u={attempted:!0,ok:!1,latency_ms:Date.now()-f,error:{code:k,message:w.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(`
|
|
1
|
+
import{spawn as W,spawnSync as Ce,execSync as j,execFile as ke}from"node:child_process";import{promisify as Ae}from"node:util";import{randomUUID as H}from"node:crypto";import{killProcessGroup as O,hasChildProcesses as Ie}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 xe,constants as ie,watch as Me}from"node:fs";import{mkdir as M,readFile as E,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 x,tmpdir as Fe}from"node:os";import V from"node:net";import{EventEmitter as re}from"node:events";import{fileURLToPath as Oe}from"node:url";let U=null;if(process.platform==="win32")try{U=await import("node-pty")}catch{U=null}import{resolveRuntimePaths as _}from"../../core/config/index.js";import{SESSION_MODE_IDS as N}from"./protocol-contract.js";import{log as a}from"../../core/log/index.js";import{ActivityStatusManager as Ne}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 Je,isGrixInternalToolName as Ge,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,extractTextBlocksAfterOffset as Ye,hasTerminalToolResultAfterOffset as oe,probeSessionTurnState as le,resolveSessionJsonlPath as Y}from"./usage-parser.js";import{readSettingsEnv as Xe}from"./model-list.js";import{resolveCliPath as Ke,getCliVersion as Ze}from"../../core/util/cli-probe.js";const F="grix";function et(u){if(!Array.isArray(u))return;const e=u.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(u){let e;try{e=JSON.parse(u)}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 d=et(r.options),f=r.multiSelect===!0||r.multi_select===!0;i.push({header:l,prompt:c,...d?{options:d}:{},...f?{multi_select:!0}:{}})}return i.length>0?i:null}function tt(u){if(!u||typeof u!="string")return null;try{const e=JSON.parse(u);return(typeof e.plan=="string"?e.plan.trim():"")||null}catch{return null}}const it=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],st=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],nt=/API Error:\s*400.*server_tool_use\.id/,de=300*1e3,rt=1800*1e3,ue=60*1e3,he=600*1e3,at=300*1e3,ot="The model's tool call could not be parsed (retry also failed).",lt="[\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",ct="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.",dt=2*1e3,X=90*1e3,pe=1800*1e3,fe=15*1e3,ut=300*1e3,me=10*1e3,ht=30*1e3,pt=30*1e3;function ft(u){try{return process.kill(u,0),!0}catch(e){return e.code==="EPERM"}}const mt=12e4,vt=3e4,gt=3e3,J=1e3,ve=45e3,yt=18e3,K=8192;let ge=!1;const Z=new Set;async function wt(u=[]){const e=new Set(u.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(u){u>0&&Z.delete(u)}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.",'Both your plain text and the reply tool are delivered to the user. You are talking directly to the user, so address them in the second person ("you"); do not slip into third-person narration about them (e.g. "\u5DF2\u7ECF\u628A\u7ED3\u8BBA\u56DE\u590D\u7ED9\u8001\u90ED\u4E86") \u2014 say it straight to them ("\u7ED3\u8BBA\u662F\u2026\u2026").',"While you work, use plain text for short progress notes. Deliver your final conclusion \u2014 the answer the user is waiting for \u2014 through the reply tool; that is the message the user treats as your result. Put the conclusion only in the reply tool, not also as plain text, so it is not sent twice. The connector automatically quotes the message you are answering, so you normally do not need to set reply_to (set it only to quote a different earlier message).","If you intentionally do not want to send any visible message, 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 deliver the actual result through the reply tool. During a long-running task, write a short plain-text progress note when you reach a meaningful milestone so the user can see progress as it happens.',"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 we(u){return String(u??"").trim().toLowerCase()===N.approval?N.approval:N.fullAuto}class Ee 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;textForwardSeq=0;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=_(),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&&(O(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{}O(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&&!ft(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 Ie(e,t)}async probe(e){const t=this.config.command||"claude",i=await Ke(t),s=i!==null;let n=null,r;if(s){const f=await Ze(t);n=f.version,f.error&&(r=f.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const l=(Xe().ANTHROPIC_BASE_URL??process.env.ANTHROPIC_BASE_URL??"").trim()||null,c=(this.sessionState?.model?.id??(process.env.ANTHROPIC_MODEL??"").trim())||null;let d={attempted:!1,ok:!1,latency_ms:null};if(e?.conversation&&s){const f=Date.now();try{await Ae(ke)(t,["-p","ping","--output-format","json","--max-turns","1"],{timeout:e.timeoutMs??8e3,encoding:"utf-8"}),d={attempted:!0,ok:!0,latency_ms:Date.now()-f}}catch(p){const w=p,k=w.killed?"conversation_timeout":"conversation_failed";d={attempted:!0,ok:!1,latency_ms:Date.now()-f,error:{code:k,message:w.message??String(p)}}}}return{cli:{command:t,installed:s,path:i,version:n,...r?{error:r}:{}},conversation:d,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(`
|
|
2
2
|
`):"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}
|
|
3
3
|
`)}finally{xe(r)}}else this.claudeProcess?.stdin?.write(`${s}
|
|
4
|
-
`);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=_(),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=_(),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(`
|
|
5
|
-
`)}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=_().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 S=String(b).trim().toLowerCase();return S!=="--dangerously-load-development-channels"&&S!=="--session-id"&&S!=="--resume"}),o=String(i.cwd??"").trim(),l=String(i.modelId??"").trim(),c=we(i.modeId);await this.ensureWorkspaceTrust(o),await this.ensureClaudeOnboardingFlags(o),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(o),await this.ensureStdioMcpServer();const
|
|
4
|
+
`);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=_(),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 d=this.pendingQuestion.get(i);return d&&this.activeEvent?.eventId===d.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=_(),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(`
|
|
5
|
+
`)}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=_().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 S=String(b).trim().toLowerCase();return S!=="--dangerously-load-development-channels"&&S!=="--session-id"&&S!=="--resume"}),o=String(i.cwd??"").trim(),l=String(i.modelId??"").trim(),c=we(i.modeId);await this.ensureWorkspaceTrust(o),await this.ensureClaudeOnboardingFlags(o),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(o),await this.ensureStdioMcpServer();const d=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 w=_e(p,o||void 0),k=this.buildSingleTurnPrompt(e,!w),v=[...r,"-p","--output-format","stream-json","--include-partial-messages",...w?["--resume",p]:["--session-id",p]];Ee.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===N.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,S)=>{const T=W(n,v,{cwd:o||process.cwd(),env:d,stdio:["ignore","pipe","pipe"],detached:!0,windowsHide:!0});let g="",m=0,P=!1;const A=`fallback_${e.event_id}_${Date.now()}`;let $="";T.stdout?.setEncoding("utf8"),T.stdout?.on("data",R=>{$+=R;let I=$.indexOf(`
|
|
6
6
|
`);for(;I>=0;){const Pe=$.slice(0,I);$=$.slice(I+1);const te=this.extractSingleTurnDelta(Pe);te&&(m+=1,P=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,te,m,!1,A,e.quoted_message_id)),I=$.indexOf(`
|
|
7
|
-
`)}}),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=>S(R)),T.on("close",R=>{if($.trim()){const I=this.extractSingleTurnDelta($);I&&(m+=1,P=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,I,m,!1,A,e.quoted_message_id))}if(R!==0&&!P){const I=g.trim();S(new Error(I||`claude single-turn exited with code ${R}`));return}m+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",m,!0,A,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=we(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=_e(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 w=!1,k="",v=null;try{t(),this.notifyPort=await wt([this.internalApiPort]),w=!0,t(),a.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const S=i.pluginDir||await this.ensureClaudePluginDir(),T=p?["--resume",f]:["--session-id",f],g=[...r,"--name",`grix-${this.sessionId}`,...T,"--plugin-dir",S,"--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===N.fullAuto&&g.push("--dangerously-skip-permissions"),g.push("--dangerously-load-development-channels",`server:${F}`),process.platform==="win32"){const m=ye.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),P=At(this.claudeCliSessionId);await bt(P,m),g.push("--append-system-prompt-file",P)}else g.push("--append-system-prompt",ye);if(process.platform==="win32")if(U){const m=Tt(),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 A=U.spawn(m,P,{name:"xterm-256color",cols:120,rows:30,cwd:u,env:o,useConpty:!0,conptyInheritCursor:!1});this.claudePty=A,this.claudeChildPid=A.pid,this.startPtyAutoConfirm(A),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(Fe(),`grix-claude-${H()}`);await M(m,{recursive:!0}),this.expectRunDir=m;const{expectPath:P,pidPath:A}=await Et(m,n,g);k=A,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 Ct(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(S){if(v?.pid&&O(v,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===v&&(this.claudeProcess=null),this.claudeChildPid=0,w&&this.releaseNotifyPortReservation(),this.stopMcpServer(),S}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.isEventAnswered(),s=i?"responded":"failed",n=i?void 0:"Claude process exited";a.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, answered=${i}, sending ${s}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,s,n),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.isEventAnswered(),s=i?"responded":"failed",n=i?void 0:"Claude process exited";a.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, answered=${i}, sending ${s}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,s,n),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.isEventAnswered()?"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()},gt))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),it.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,`
|
|
7
|
+
`)}}),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=>S(R)),T.on("close",R=>{if($.trim()){const I=this.extractSingleTurnDelta($);I&&(m+=1,P=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,I,m,!1,A,e.quoted_message_id))}if(R!==0&&!P){const I=g.trim();S(new Error(I||`claude single-turn exited with code ${R}`));return}m+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",m,!0,A,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=we(s.modeId),c=String(s.modelId??"").trim(),d=String(s.cwd??"").trim();this.claudeSessionCwd=d;const f=String(s.claudeSessionId??"").trim()||H();this.claudeCliSessionId=f,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(f);const p=_e(f,d||void 0);if(p||(ee(f,[]),z(f)),this.runtimeResolver&&!d)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!d)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(d),await this.ensureClaudeOnboardingFlags(d),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(d);let w=!1,k="",v=null;try{t(),this.notifyPort=await wt([this.internalApiPort]),w=!0,t(),a.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const S=i.pluginDir||await this.ensureClaudePluginDir(),T=p?["--resume",f]:["--session-id",f],g=[...r,"--name",`grix-${this.sessionId}`,...T,"--plugin-dir",S,"--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===N.fullAuto&&g.push("--dangerously-skip-permissions"),g.push("--dangerously-load-development-channels",`server:${F}`),process.platform==="win32"){const m=ye.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),P=At(this.claudeCliSessionId);await bt(P,m),g.push("--append-system-prompt-file",P)}else g.push("--append-system-prompt",ye);if(process.platform==="win32")if(U){const m=Tt(),P=["/c",n,...g];a.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${d} mode=${l} ${p?"resume":"new"} ${n} ${g.join(" ")}`);const A=U.spawn(m,P,{name:"xterm-256color",cols:120,rows:30,cwd:d,env:o,useConpty:!0,conptyInheritCursor:!1});this.claudePty=A,this.claudeChildPid=A.pid,this.startPtyAutoConfirm(A),v=null}else a.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${d} mode=${l} ${p?"resume":"new"} ${n} ${g.join(" ")}`),v=W(n,g,{cwd:d,env:o,stdio:["pipe","pipe","pipe"],detached:!0,shell:!0,windowsHide:!0});else{a.info("claude-adapter",`Spawning via expect PTY: cwd=${d} 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(Fe(),`grix-claude-${H()}`);await M(m,{recursive:!0}),this.expectRunDir=m;const{expectPath:P,pidPath:A}=await Et(m,n,g);k=A,t(),v=W("/usr/bin/expect",[P],{cwd:d,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 Ct(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(S){if(v?.pid&&O(v,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===v&&(this.claudeProcess=null),this.claudeChildPid=0,w&&this.releaseNotifyPortReservation(),this.stopMcpServer(),S}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.isEventAnswered(),s=i?"responded":"failed",n=i?void 0:"Claude process exited";a.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, answered=${i}, sending ${s}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,s,n),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.isEventAnswered(),s=i?"responded":"failed",n=i?void 0:"Claude process exited";a.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, answered=${i}, sending ${s}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,s,n),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.isEventAnswered()?"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()},gt))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),it.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,`
|
|
8
8
|
|
|
9
9
|
Error: Claude authentication failed -- please re-login`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude authentication failed -- please re-login"),this.clearActiveEvent())),st.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,`
|
|
10
10
|
|
|
11
11
|
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())),nt.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(_().dataDir,"claude-plugin"),i=h(t,".claude-plugin"),s=h(i,"plugin.json"),n=h(e,".claude-plugin","plugin.json");await M(i,{recursive:!0});let r="";try{r=await E(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)}
|
|
12
|
-
`}let o="";try{o=await E(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=S=>`"${String(S).replace(/"/g,'\\"')}"`,
|
|
12
|
+
`}let o="";try{o=await E(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=S=>`"${String(S).replace(/"/g,'\\"')}"`,d=S=>`${c(process.execPath)} ${c(h(l,S))}`,f=`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],Elicitation:[{hooks:[{type:"command",command:d("elicitation-hook.js")}]}],ElicitationResult:[{matcher:"",hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],UserPromptSubmit:[{hooks:[{type:"command",command:d("user-prompt-submit-hook.js")}]}],PreToolUse:[{matcher:"ExitPlanMode",hooks:[{type:"command",command:d("approve-plan-hook.js")}]},{matcher:"",hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],PostToolUse:[{matcher:"",hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],PostToolUseFailure:[{matcher:"",hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],PermissionRequest:[{matcher:"",hooks:[{type:"command",command:d("permission-hook.js")}]}],Notification:[{matcher:"idle_prompt",hooks:[{type:"command",command:d("notification-hook.js")}]}],PermissionDenied:[{matcher:"",hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],Stop:[{hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],StopFailure:[{hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],PreCompact:[{hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],PostCompact:[{hooks:[{type:"command",command:d("lifecycle-hook.js")}]}],ConfigChange:[{hooks:[{type:"command",command:d("lifecycle-hook.js")}]}]}},null,2)}
|
|
13
13
|
`,p=h(t,"hooks"),w=h(p,"hooks.json");await M(p,{recursive:!0});let k="";try{k=await E(w,"utf8")}catch{}k!==f&&(await C(w,f,"utf8"),a.info("claude-adapter",`Wrote Claude hooks config: ${w}`));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=kt(this.claudeCliSessionId);await It(c,n,l),this.claudeMcpConfigPath=c,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=Oe(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>=J)return;await new Promise(r=>setTimeout(r,J-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}
|
|
14
14
|
`;"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>=J)return;await new Promise(c=>setTimeout(c,J-l));return}const n=Date.now()-t,r=this.claudePty?yt:12e3;if(this.alive&&this.mcpServerReady&&n>r){a.info("claude-adapter",`Windows ${this.claudePty?"PTY":"shell"} fallback: assuming channel ready after ${n}ms (no stdout detection, MCP server connected)`),this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.clearPendingMcpFailureTimer(),this.sendDeferredModelSwitch();return}await new Promise(o=>setTimeout(o,500))}}finally{clearInterval(s)}throw new Error(`Windows channel ready check timed out within ${e}ms`)}pushNotification(e,t){if(!this.mcpServerReady||this.notifyPort<=0)return;const i=JSON.stringify({jsonrpc:"2.0",method:e,params:t});!this.notifySocket||this.notifySocket.destroyed?(this.notifySocket=V.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{this.notifySocket.write(i+`
|
|
15
15
|
`)}),this.notifySocket.on("error",s=>{a.error("claude-adapter",`Notify socket error: ${s.message}`),this.notifySocket=null}),this.notifySocket.on("close",()=>{this.notifySocket=null})):this.notifySocket.write(i+`
|
|
16
|
-
`)}stopMcpServer(){if(this.mcpServerReady=!1,this.notifySocket){try{this.notifySocket.destroy()}catch{}this.notifySocket=null}if(this.mcpServerProcess){try{this.mcpServerProcess.kill("SIGTERM")}catch{}this.mcpServerProcess=null}}releaseNotifyPortReservation(){St(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const t=this.claudeProcess,i=this.claudePty,s=this.claudeChildPid;if(this.claudeProcess=null,this.claudePty=null,this.claudeChildPid=0,this.spawnPromise=null,this.alive=!1,this.stopMcpServer(),this.releaseNotifyPortReservation(),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.sessionIdConflictDetected=!1,this.pendingPermissions.size>0){const n=_(),r=new q(n.permissionRequestsDir);for(const[o]of this.pendingPermissions)r.resolveRequest(o,"deny").catch(()=>{});this.pendingPermissions.clear()}if(i)try{i.kill()}catch{}if(t?.pid&&O(t,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(t?.pid||i||s>0){const n=s,r=t,o=i;setTimeout(()=>{if(o)try{o.kill()}catch{}if(r?.pid&&O(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}a.info("claude-adapter",`Claude process killed (reason=${e}, pid=${s}, expectPid=${t?.pid})`)}tryRecoverSessionIdConflict(){if(!this.sessionIdConflictDetected||this.stopped||!this.activeEvent||this.isEventAnswered())return!1;const e=this.activeEvent.eventId;if(this.sessionIdConflictRetriedEventIds.has(e))return this.sessionIdConflictDetected=!1,!1;this.sessionIdConflictRetriedEventIds.add(e),this.sessionIdConflictDetected=!1;const t=this.activeEvent.rawEvent;return a.warn("claude-adapter",`Detected Claude session-id conflict, auto-retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(t),!0}recoverMalformedToolCall(e){if(this.stopped||!this.activeEvent||this.isEventAnswered()||!this.claudeCliSessionId||!this.claudeSessionCwd)return"none";const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!t||t.stopReason!=="stop_sequence"||t.text.trim()!==ot)return"none";if(this.malformedToolRetriedEventIds.has(e))return a.warn("claude-adapter",`Malformed tool-call retry exhausted for ${e}, sending clean fallback`),"exhausted";this.malformedToolRetriedEventIds.add(e);const i={...this.activeEvent.rawEvent,content:lt};return a.warn("claude-adapter",`Detected malformed tool-call, silently retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(i),"retried"}async handleInternalInvoke(e,t,i){if(e==="event_tool_call"){const s=String(t.tool_name??""),n=ze(s,{...t.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const l=this.activeEvent,c=String(n.event_id??"").trim();c===""?n.event_id=l.eventId:(s==="grix_reply"||s==="grix_complete")&&c!==l.eventId&&(a.warn("claude-adapter",`${s}: event_id mismatch, correcting: supplied=${c}, active=${l.eventId}`),n.event_id=l.eventId),String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId)}else if(this.lastClearedEvent&&s==="grix_reply"){const l=this.lastClearedEvent;Date.now()-l.ts<mt&&(String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId),n.event_id="",a.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${l.eventId} (cleared ${(Date.now()-l.ts)/1e3}s ago)`))}s==="grix_reply"&&String(n.event_id??"").trim()!==""&&this.completedEventIds.has(String(n.event_id??"").trim())&&(a.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(n.event_id??"").trim()}`),n.event_id="");const o=Be(s,n);if(!o.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${o.error}`);if(r.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(s==="grix_access_control")return this.executeAccessControl(n);if(qe(s)){const l=this.activeEvent;l&&(l.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const c=Ue(r,s,n);if(c.isError)throw new Error(c.content[0]?.text??"event tool failed");if(i?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(s,n),JSON.parse(c.content[0]?.text??"null")}finally{if(l&&(l.toolCallInFlight=!1,l.pendingStopHook&&this.activeEvent===l)){const c=l.pendingStopHook;l.pendingStopHook=void 0,this.finalizeActiveEvent(c)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${s}`)}return this.bridgeCallbacks.agentInvoke(e,t)}getEventToolHandle(){const e=this;return{status:"ready",getStatusSnapshot:()=>({status:"ready",connectedAt:Date.now(),reconnectAttempts:0}),sendEventAck:t=>{e.bridgeCallbacks.sendEventAck(t.event_id,t.session_id??"")},sendStreamChunk:t=>{e.bridgeCallbacks.sendStreamChunk(t.event_id??"",t.session_id,t.delta_content??"",Number(t.chunk_seq??0)||1,t.is_finish===!0,t.client_msg_id,t.quoted_message_id)},sendMsg:t=>{if(typeof e.bridgeCallbacks.sendDirectMessage!="function"){a.warn("claude-adapter","sendDirectMessage callback not provided, dropping direct message");return}e.bridgeCallbacks.sendDirectMessage({sessionId:t.session_id,clientMsgId:t.client_msg_id,content:t.content,quotedMessageId:t.quoted_message_id})},sendEventResult:t=>{e.bridgeCallbacks.sendEventResult(t.event_id,t.status,t.msg,t.code)},sendSessionActivitySet:t=>{}}}postProcessEventToolCall(e,t){const i=String(t.event_id??"").trim();if(!i||this.activeEvent?.eventId!==i){a.warn("claude-adapter",`postProcessEventToolCall: event_id mismatch (tool=${e}, eventId=${i}, activeEventId=${this.activeEvent?.eventId??"none"})`);return}if(e==="grix_complete"){this.completedEventIds.set(i,Date.now()),this.clearActiveEvent();return}if(e==="grix_reply"){const s=!this.activeEvent.replied;this.activeEvent.replied=!0;const n=String(t.text??"");n.length>0&&(this.activeEvent.lastReplyText=n),this.markActiveEventActivity(i,String(t.session_id??"").trim()||void 0),s&&(this.activeEvent.repliedAt=Date.now(),this.startPostReplyDeadline(i),this.startPostReplyJsonlWatcher(i))}}async executeAccessControl(e){const t=String(e.action??""),i=Je[t];if(!i)throw new Error(`\u672A\u77E5 access_control action: ${t}`);const s={};return e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy),this.bridgeCallbacks.agentInvoke("claude_access_control",{verb:i,payload:s},3e4)}resolveSessionRuntime(){if(!this.runtimeResolver)return{};try{return this.runtimeResolver(this.sessionId)??{}}catch(e){throw new Error(`resolve session runtime failed: ${e instanceof Error?e.message:String(e)}`)}}async validatePluginDir(e){const t=String(e??"").trim();if(!t)return;let i;try{i=await ne(t)}catch{throw new Error(`pluginDir is not accessible: ${t}`)}if(!i.isDirectory())throw new Error(`pluginDir is not a directory: ${t}`);const s=h(t,".mcp.json");try{(await ne(s)).isFile()&&(await se(s),a.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=h(x(),".claude.json");try{const i=await E(t,"utf8"),s=JSON.parse(i);if(s.projects?.[e]?.hasTrustDialogAccepted===!0)return;s.projects||(s.projects={}),s.projects[e]||(s.projects[e]={}),s.projects[e].hasTrustDialogAccepted=!0,await C(t,JSON.stringify(s),"utf8"),a.info("claude-adapter",`Pre-trusted workspace: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${i}`)}}async ensureSkipDangerousPermissionPrompt(){const e=h(x(),".claude","settings.json");try{let t={};try{t=JSON.parse(await E(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await M(h(x(),".claude"),{recursive:!0}),await C(e,JSON.stringify(t,null,2),"utf8"),a.info("claude-adapter","Set skipDangerousModePermissionPrompt=true in user settings to skip BypassPermissions dialog")}catch(t){a.warn("claude-adapter",`Failed to set skipDangerousModePermissionPrompt: ${t}`)}}async isChannelGateClosed(){if(this.channelGateClosed)return!0;try{const e=await E(h(x(),".claude.json"),"utf8"),i=JSON.parse(e).cachedGrowthBookFeatures;return!i||Object.keys(i).length===0?!1:i.tengu_harbor!==!0}catch{return!1}}async ensureClaudeOnboardingFlags(e){const t=h(x(),".claude.json");try{let i;try{const r=await E(t,"utf8");i=JSON.parse(r)}catch{i={}}let s=!1;i.hasCompletedOnboarding||(i.hasCompletedOnboarding=!0,i.lastOnboardingVersion||(i.lastOnboardingVersion="2.1.31"),s=!0),i.projects||(i.projects={});const n=i.projects;if(n[e]||(n[e]={}),n[e].hasCompletedProjectOnboarding||(n[e].hasCompletedProjectOnboarding=!0,s=!0),!s)return;await C(t,JSON.stringify(i,null,2),"utf8"),a.info("claude-adapter",`Marked Claude onboarding complete: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to mark Claude onboarding complete: ${i}`)}}async injectStatusLineSettings(e){try{const t=this.resolveProjectRoot(),i=h(t,"dist","scripts","status-line-forwarder.js"),s=h(e,".claude"),n=h(s,"settings.json");await M(s,{recursive:!0});let r={};try{r=JSON.parse(await E(n,"utf8"))}catch{}const l={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=l;const c=`${JSON.stringify(r,null,2)}
|
|
17
|
-
`;await C(n,c,"utf8"),a.info("claude-adapter",`Injected statusLine settings: ${n}`)}catch(t){a.warn("claude-adapter",`Failed to inject statusLine settings: ${t instanceof Error?t.message:t}`)}}async ensureUserMcpServer(e,t,i){const s=this.resolveServerEntryPath(t),n=process.execPath,r=[s],o=h(x(),".claude.json");let l=null;try{const p=await E(o,"utf8");l=JSON.parse(p)?.mcpServers?.[F]??null}catch{}if(l&&String(l.type||"stdio").trim()==="stdio"&&String(l.command??"").trim()===n&&Array.isArray(l.args)&&l.args.length===r.length&&l.args.every((p,w)=>p===r[w]))return;a.info("claude-adapter",`Registering user-scoped MCP server: ${F} -> ${n} ${r.join(" ")}`);try{j(`${e} mcp remove -s user ${F}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"})}catch{}const c=["mcp","add","--scope","user",F,"--",n,...r],u=process.platform==="win32"?'"':"'",f=j(`${e} ${c.map(p=>`${u}${p}${u}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"});a.info("claude-adapter",`MCP server registered: ${f.trim()||"ok"}`)}resolveServerEntryPath(e){const t=h(e,"server","main.js");try{if(Q(t))return t}catch{}const i=h(e,"dist","index.js");try{if(Q(i))return i}catch{}throw new Error(`Cannot find grix-claude server entry in pluginDir: ${e}`)}clearActiveEvent(){const e=this.activeEvent;this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.clearActiveEventPostReplyTimer(),this.clearActiveEventPostReplyWatcher(),e&&(this.sessionIdConflictRetriedEventIds.delete(e.eventId),this.malformedToolRetriedEventIds.delete(e.eventId),this.completedEventIds.set(e.eventId,Date.now()),this.lastClearedEvent={eventId:e.eventId,sessionId:e.sessionId,ts:Date.now()},this.emit(`reply:${e.sessionId}`,{status:"completed"}),this.emit("eventDone",e.eventId)),this.activeEvent=null,this.stopComposing()}startComposing(e,t){}stopComposing(){}clearComposingTimer(){}resolveHookSignalsPath(){const e=_();return h(e.dataDir,`hook-signals-${this.sessionId}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=_().hookSignalsLogPath,i=new je(e,t);a.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new Ne({hookSignalStore:i,onActivity:s=>this.onHookActivity(s),onStop:()=>this.onClaudeTurnEnd("Stop"),onStopFailure:()=>this.onClaudeTurnEnd("StopFailure"),onCompactStart:()=>this.beginCompaction(),onCompactResult:()=>this.finishCompaction("post-compact-hook"),onPermissionRequest:(s,n)=>this.handlePermissionHookEvent(s,n)}),this.activityManager.start()}onClaudeTurnEnd(e){if(a.info("claude-adapter",`Hook activity: ${e}`),!this.activeEvent){this.stopHookBarrierSessionId&&(a.info("claude-adapter",`Late Stop hook after end_turn finalize \u2014 releasing barrier (session=${this.stopHookBarrierSessionId})`),this.clearStopHookBarrier());return}if(this.activeEvent.toolCallInFlight){a.info("claude-adapter",`Stop hook deferred: toolCallInFlight for ${this.activeEvent.eventId}`),this.activeEvent.pendingStopHook=e;return}this.finalizeActiveEvent(e)}forwardIncrementalText(e){const t=this.activeEvent;if(!t||t.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const i=t.textForwardOffset??t.jsonlBaseOffset??0,{segments:s,newOffset:n}=Ye(this.claudeCliSessionId,this.claudeSessionCwd,i);if(t.textForwardOffset=n,s.length===0)return;const r=t.sessionId;for(const o of s){const l=o.trim();if(!l)continue;const c=`txt_${e}_${this.textForwardSeq++}`;this.bridgeCallbacks.sendStreamChunk(e,r,l,1,!1,c),this.bridgeCallbacks.sendStreamChunk(e,r,"",2,!0,c),t.textForwarded||(t.textForwarded=!0,t.repliedAt===void 0&&(t.repliedAt=Date.now()),this.startPostReplyDeadline(e),this.startPostReplyJsonlWatcher(e))}this.markActiveEventActivity(e,r),a.info("claude-adapter",`Forwarded ${s.length} plain-text segment(s) for ${e}`)}flushForwardedTail(e){this.activeEvent?.eventId===e&&this.activeEvent.textForwarded&&this.forwardIncrementalText(e)}isEventAnswered(e=this.activeEvent){return!!(e&&(e.replied||e.textForwarded))}attemptRescueFromJsonl(e,t){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return a.info("claude-adapter",`Rescue skipped: no claudeCliSessionId or claudeSessionCwd for ${e}`),!1;const i=this.activeEvent?.jsonlBaseOffset,s=Ve(this.claudeCliSessionId,this.claudeSessionCwd,i);if(!s)return a.info("claude-adapter",`Rescue failed: no assistant text found in JSONL for ${e}`),!1;const n=`rescue_${e}_${Date.now()}`;return this.bridgeCallbacks.sendStreamChunk(e,t,s,1,!1,n),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,n),a.info("claude-adapter",`Rescue succeeded for event ${e}: sent ${s.length} chars from JSONL`),!0}warnUnsentFinalIfAny(e){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=this.activeEvent?.jsonlBaseOffset,i=B(this.claudeCliSessionId,this.claudeSessionCwd,t);if(!i||i.stopReason!=="end_turn")return;const s=i.text?.trim()??"";if(!s)return;const n=(this.activeEvent?.lastReplyText??"").trim();n&&(s===n||n.includes(s))||a.warn("claude-adapter",`Unsent final end_turn text for ${e} (${s.length} chars) not delivered \u2014 \u5DF2\u5E94\u7B54\u8FC7\uFF0C\u6309\u7B56\u7565\u4E0D\u8865\u53D1\uFF08\u7591\u4F3C\u56DE\u5408\u6536\u5C3E\u81EA\u8FF0\uFF1B\u82E5\u786E\u4E3A\u6F0F\u53D1\u7ED3\u8BBA\u53EF\u636E\u6B64\u6392\u67E5\uFF09`)}finalizeActiveEvent(e){if(!this.activeEvent)return;const t=this.activeEvent.eventId,i=this.activeEvent.sessionId;if(this.isEventAnswered()&&this.activeEvent.jsonlBaseOffset!==void 0&&this.claudeCliSessionId&&this.claudeSessionCwd){const n=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!n||n.stopReason!=="end_turn")if(oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset))a.info("claude-adapter",`Stop hook: terminal tool_result detected for ${t} (grix_reply as final action, no end_turn expected), finalizing as responded`);else{a.info("claude-adapter",`Stop hook suppressed for ${t}: no end_turn in JSONL after offset=${this.activeEvent.jsonlBaseOffset} \u2014 likely resume-drain hook, waiting for JSONL watcher`),this.markActiveEventActivity(t,i);return}}this.flushForwardedTail(t);let s=!1;if(!this.activeEvent.apiFormatError)if(this.activeEvent.replied||this.activeEvent.textForwarded)this.activeEvent.replied&&!this.activeEvent.textForwarded&&this.warnUnsentFinalIfAny(t);else{const n=this.recoverMalformedToolCall(t);if(n==="retried")return;n==="exhausted"?(this.bridgeCallbacks.sendStreamChunk(t,i,ct,1,!1,`mtc_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`mtc_${t}`),s=!0):s=this.attemptRescueFromJsonl(t,i)}if(this.activeEvent.replied||this.activeEvent.textForwarded||s)a.info("claude-adapter",`Stop hook received, finalizing event ${t} as responded (replied=${this.activeEvent.replied}, textForwarded=${this.activeEvent.textForwarded}, rescued=${s})`),this.bridgeCallbacks.sendEventResult(t,"responded");else{a.warn("claude-adapter",`Active event not confirmed when ${e}: ${t} apiFormatError=${!!this.activeEvent.apiFormatError}, sending failed result`);const n=this.activeEvent.apiFormatError?"Claude hit an API format error. Please start a new conversation (/grix open <dir>).":"Claude exited before completing its reply. Please try again.";this.bridgeCallbacks.sendStreamChunk(t,i,n,1,!1,`err_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`err_${t}`),this.bridgeCallbacks.sendEventResult(t,"failed",n,"agent_stop_failure")}this.clearActiveEvent(),this.armStopHookBarrier(i)}onHookActivity(e){const t=this.activeEvent?.sessionId;if(a.info("claude-adapter",`Hook activity: tool=${e?.tool_name??"(clear)"} session=${t??"(none)"}`),e&&this.activeEvent&&(this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId),this.forwardIncrementalText(this.activeEvent.eventId),(this.activeEvent.replied||this.activeEvent.textForwarded)&&this.startPostReplyDeadline(this.activeEvent.eventId)),e&&!this.activeEvent&&!this.compacting&&this.noteSelfDrivenActivity(),!!t)if(e){this.startComposing(t,e);const i=this.activeEvent;if(i){const s=e.event_name,n=e.tool_name,r=e.tool_input??"",o=n?Ge(n):!1;if(n==="ExitPlanMode"&&s==="PreToolUse")a.info("claude-adapter","ExitPlanMode detected; waiting for user decision via permission card");else if(n==="AskUserQuestion"&&s==="PreToolUse")this.handleAskUserQuestion(i,r);else if(s==="PreToolUse"&&n)r&&(this.lastPreToolInput=r),o||this.bridgeCallbacks.sendToolUse(i.eventId,i.sessionId,n,r);else if((s==="PostToolUse"||s==="PostToolUseFailure")&&n){if(n==="AskUserQuestion")return;const l=r||this.lastPreToolInput;if(this.lastPreToolInput="",!o){const c=s==="PostToolUseFailure"?`(failed) ${l}`:l;this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,c)}}}}else this.startComposing(t)}handlePermissionHookEvent(e,t){if(!this.activeEvent){a.warn("claude-adapter","PermissionRequest without active event, ignoring");return}if(e==="AskUserQuestion"){a.info("claude-adapter","Skip permission card for AskUserQuestion; handled by agent_question card flow");return}const i=this.activeEvent,s=_();new q(s.permissionRequestsDir).listPending().then(r=>{const o=r.length>0?r[r.length-1]:null;if(!o){a.warn("claude-adapter","No pending permission request found in store");return}const l=o.request_id;this.pendingPermissions.set(l,{eventId:i.eventId,sessionId:i.sessionId});const c=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const p=tt(c);p&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,p)}const u=e==="ExitPlanMode"?"":c.slice(0,100),f=u?`${e}: ${u}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:l,toolName:e,toolTitle:f}),a.info("claude-adapter",`Sent permission card: approvalId=${l} tool=${e}`)}).catch(r=>{a.warn("claude-adapter",`Failed to send permission card: ${r}`)})}handleAskUserQuestion(e,t){const i=ce(t);if(!i){a.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=_(),n=new ae(s.questionRequestsDir);n.listPending().then(async r=>{const o=[...r].reverse().filter(u=>String(u.session_id??"").trim()===this.claudeCliSessionId),l=o.find(u=>String(u.event_id??"").trim()===e.eventId)??o.find(u=>String(u.event_id??"").trim()==="")??o[0]??null,c=l?.request_id??`fallback-${Date.now()}`;l&&String(l.event_id??"").trim()===""&&await n.updateRequest(l.request_id,{event_id:e.eventId}),this.pendingQuestion.set(c,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0,this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:c,mode:"form",questions:i}),a.info("claude-adapter",`Sent agent_question card: request_id=${c} questions=${i.length}`)}).catch(()=>{a.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return ce(e)}parseAskUserQuestionInput(e){let t;try{t=JSON.parse(e)}catch{return null}const i=[],s=t.questions;if(!Array.isArray(s)||s.length===0)return null;for(let n=0;n<s.length;n++){const r=s[n];if(!r||typeof r!="object")continue;const o=String(r.header??r.question??`Question ${n+1}`),l=String(r.prompt??""),c=Array.isArray(r.options)?r.options:void 0,u=r.multiSelect===!0;i.push({header:o,prompt:l,...c&&c.length>0?{options:c}:{},...u?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-rt;for(const[t,i]of this.completedEventIds.entries())i<e&&this.completedEventIds.delete(t)}markActiveEventActivity(e,t){const i=this.activeEvent;i&&(e&&i.eventId!==e||t&&i.sessionId!==t||(this.resetActiveEventIdleTimer(i.eventId),this.isEventAnswered(i)||this.resetActiveEventHardTimer(i.eventId)))}clearActiveEventIdleTimer(){this.activeEventIdleTimer&&(clearTimeout(this.activeEventIdleTimer),this.activeEventIdleTimer=null)}clearActiveEventHardTimer(){this.activeEventHardTimer&&(clearTimeout(this.activeEventHardTimer),this.activeEventHardTimer=null)}shouldExtendByLiveness(e){const t=this.activeEvent;if(!t||t.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd||!this.claudeProcess&&!this.claudePty)return!1;if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<fe)return t.livenessExtendStartAt=Date.now(),!0;const i=le(this.claudeCliSessionId,this.claudeSessionCwd,t.jsonlBaseOffset);if(i.freshMs===null||!(t.jsonlBaseOffset===void 0?i.freshMs<X:i.lastStopReason!==null?i.lastStopReason!=="end_turn":i.freshMs<X))return!1;if(i.freshMs<X)t.livenessExtendStartAt=Date.now();else{const n=t.livenessExtendStartAt??Date.now();if(t.livenessExtendStartAt===void 0&&(t.livenessExtendStartAt=n),Date.now()-n>pe)return a.warn("claude-adapter",`Liveness extension budget exhausted for ${e} (no JSONL writes for ${Math.round((Date.now()-n)/6e4)}min), allowing close`),!1}return a.info("claude-adapter",`Liveness check: turn in progress for ${e} (lastStopReason=${i.lastStopReason??"none"}, freshMs=${i.freshMs}), extending`),!0}captureEventJsonlBaseOffset(e){const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=Y(this.claudeCliSessionId,this.claudeSessionCwd);t.jsonlBaseOffset=D(i)?L(i).size:0}catch{t.jsonlBaseOffset=0}}noteSelfDrivenActivity(){!this.selfDrivenActive&&this.lastClearedEvent&&Date.now()-this.lastClearedEvent.ts<pt||(this.selfDrivenLastSignalAt=Date.now(),!this.selfDrivenActive&&(this.selfDrivenActive=!0,a.info("claude-adapter",`Self-driven activity detected for session ${this.sessionId} (no active event), debouncing before reporting working state`),this.selfDrivenReportTimer=setTimeout(()=>{this.selfDrivenReportTimer=null,!(!this.selfDrivenActive||this.stopped||this.activeEvent)&&(this.selfDrivenReported=!0,a.info("claude-adapter",`Self-driven working state confirmed for session ${this.sessionId} (sustained >${me/1e3}s), reporting`),this.emit("sessionActivity",this.sessionId??"",!0))},me),this.selfDrivenReportTimer.unref(),this.selfDrivenSweepTimer=setInterval(()=>this.sweepSelfDriven(),ht),this.selfDrivenSweepTimer.unref()))}sweepSelfDriven(){if(!this.selfDrivenActive)return;if(this.stopped||this.activeEvent){this.stopSelfDriven();return}const e=Date.now()-this.selfDrivenLastSignalAt;if(e<ut){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0);return}if(this.claudeCliSessionId&&this.claudeSessionCwd){const t=le(this.claudeCliSessionId,this.claudeSessionCwd),i=t.lastStopReason!==null&&t.lastStopReason!=="end_turn",s=t.freshMs!==null&&t.freshMs<pe;if(i&&s){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0);return}}a.info("claude-adapter",`Self-driven activity ended for session ${this.sessionId} (quiet for ${Math.round(e/1e3)}s)`),this.stopSelfDriven()}stopSelfDriven(){this.selfDrivenSweepTimer&&(clearInterval(this.selfDrivenSweepTimer),this.selfDrivenSweepTimer=null),this.selfDrivenReportTimer&&(clearTimeout(this.selfDrivenReportTimer),this.selfDrivenReportTimer=null),this.selfDrivenActive&&(this.selfDrivenActive=!1,this.selfDrivenReported&&(this.selfDrivenReported=!1,this.emit("sessionActivity",this.sessionId??"",!1)))}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.activeEvent?.toolCallInFlight){a.info("claude-adapter",`Idle timeout skipped: toolCallInFlight for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.activeEvent?.awaitingUserQuestion){a.info("claude-adapter",`Idle timeout skipped: awaitingUserQuestion for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.pendingPermissions.size>0){a.info("claude-adapter",`Idle timeout skipped: pendingPermissions=${this.pendingPermissions.size} for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.shouldExtendByLiveness(e)){this.resetActiveEventIdleTimer(e);return}this.finalizeStuckActiveEvent(e,"idle")}},ue)}finalizeStuckActiveEvent(e,t){const i=this.isEventAnswered(),s=i?"responded":"failed",n=t==="idle"?ue:he,r=i?void 0:t==="idle"?`agent idle for ${n/1e3}s`:`agent exceeded max duration ${n/1e3}s`,o=i?void 0:t==="idle"?"agent_idle_timeout":"agent_hard_timeout";a.error("claude-adapter",`Active event ${t} timeout (${n/1e3}s): ${e}, answered=${i}, sending ${s}`),this.flushForwardedTail(e),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,s,r,o),this.clearActiveEvent(),s==="failed"&&this.emit("stuck")}resetActiveEventHardTimer(e){this.clearActiveEventHardTimer(),this.activeEventHardTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.shouldExtendByLiveness(e)){this.resetActiveEventHardTimer(e);return}this.finalizeStuckActiveEvent(e,"hard")}},he)}clearActiveEventPostReplyTimer(){this.activeEventPostReplyTimer&&(clearTimeout(this.activeEventPostReplyTimer),this.activeEventPostReplyTimer=null)}clearActiveEventPostReplyWatcher(){if(this.activeEventPostReplyWatcher){try{this.activeEventPostReplyWatcher.close()}catch{}this.activeEventPostReplyWatcher=null}this.activeEventPostReplyPoll&&(clearInterval(this.activeEventPostReplyPoll),this.activeEventPostReplyPoll=null)}startPostReplyJsonlWatcher(e){if(this.clearActiveEventPostReplyWatcher(),!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=Y(this.claudeCliSessionId,this.claudeSessionCwd);if(!this.activeEvent?.sessionId)return;if(!D(t)){a.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=L(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;try{this.activeEventPostReplyWatcher=Me(t,()=>{n&&clearTimeout(n),n=setTimeout(()=>this.finalizeIfEndTurn(e),300)}),this.activeEventPostReplyPoll=setInterval(()=>this.finalizeIfEndTurn(e),dt),a.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(r){a.warn("claude-adapter",`JSONL watcher start failed for ${e}: ${r}`)}}finalizeIfEndTurn(e){if(this.stopped||this.activeEvent?.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(t&&t.stopReason==="end_turn"){a.info("claude-adapter",`JSONL watcher: end_turn detected for ${e}, finalizing as responded`);const i=this.activeEvent.sessionId;this.flushForwardedTail(e),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent(),this.armStopHookBarrier(i);return}if(this.activeEvent?.replied&&this.activeEvent.jsonlBaseOffset!==void 0&&oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset,!0)){if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<fe)return;a.info("claude-adapter",`JSONL poll: terminal tool_result detected for ${e} (file quiet, no end_turn), finalizing as responded`),this.flushForwardedTail(e),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()}}startPostReplyDeadline(e){this.clearActiveEventPostReplyTimer(),this.clearActiveEventHardTimer(),this.activeEventPostReplyTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e||!this.activeEvent?.replied&&!this.activeEvent?.textForwarded)return;if(this.shouldExtendByLiveness(e)){this.startPostReplyDeadline(e);return}const t=this.activeEvent.repliedAt,i=Math.round((Date.now()-t)/1e3);a.info("claude-adapter",`Post-reply deadline reached for ${e} (${i}s since reply), completing as responded`),this.flushForwardedTail(e),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()},at)}beginCompaction(){this.compacting||(this.compacting=!0,a.info("claude-adapter","Compaction started; gating input"),this.emit("pauseIntake","compaction"),this.resetCompactingActivityTimer())}resetCompactingActivityTimer(){this.compactingTimer&&clearTimeout(this.compactingTimer),this.compactingTimer=setTimeout(()=>{a.warn("claude-adapter","Compaction stall: no PTY activity for 90s, emitting stuck"),this.emit("stuck")},9e4),this.compactingTimer.unref?.()}finishCompaction(e){if(!this.compacting)return;this.compacting=!1,this.compactingTimer&&(clearTimeout(this.compactingTimer),this.compactingTimer=null),a.info("claude-adapter",`Compaction finished (${e}); resuming intake`),this.emit("resumeIntake","compaction");const t=this.compactionDoneResolver;this.compactionDoneResolver=null,t?.(e)}armStopHookBarrier(e){this.stopHookBarrierTimer&&clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierSessionId=e,this.emit("pauseIntake","barrier"),this.stopHookBarrierTimer=setTimeout(()=>{this.stopHookBarrierSessionId===e&&(a.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier())},vt)}clearStopHookBarrier(){const e=this.stopHookBarrierSessionId!==null;this.stopHookBarrierSessionId=null,this.stopHookBarrierTimer&&(clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierTimer=null),e&&this.emit("resumeIntake","barrier")}clearPendingMcpFailureTimer(){this.pendingMcpFailureTimer&&(clearTimeout(this.pendingMcpFailureTimer),this.pendingMcpFailureTimer=null)}markMcpStartupFailure(){if(!this.mcpStartupFailureHandled&&(this.mcpStartupFailureHandled=!0,this.mcpChannelBroken=!0,a.error("claude-adapter","Claude reported blocking MCP server startup failure"),this.activeEvent)){const e=this.activeEvent.eventId,t=this.activeEvent.sessionId;this.bridgeCallbacks.sendStreamChunk(e,t,`
|
|
16
|
+
`)}stopMcpServer(){if(this.mcpServerReady=!1,this.notifySocket){try{this.notifySocket.destroy()}catch{}this.notifySocket=null}if(this.mcpServerProcess){try{this.mcpServerProcess.kill("SIGTERM")}catch{}this.mcpServerProcess=null}}releaseNotifyPortReservation(){St(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const t=this.claudeProcess,i=this.claudePty,s=this.claudeChildPid;if(this.claudeProcess=null,this.claudePty=null,this.claudeChildPid=0,this.spawnPromise=null,this.alive=!1,this.stopMcpServer(),this.releaseNotifyPortReservation(),this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.sessionIdConflictDetected=!1,this.pendingPermissions.size>0){const n=_(),r=new q(n.permissionRequestsDir);for(const[o]of this.pendingPermissions)r.resolveRequest(o,"deny").catch(()=>{});this.pendingPermissions.clear()}if(i)try{i.kill()}catch{}if(t?.pid&&O(t,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(t?.pid||i||s>0){const n=s,r=t,o=i;setTimeout(()=>{if(o)try{o.kill()}catch{}if(r?.pid&&O(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}a.info("claude-adapter",`Claude process killed (reason=${e}, pid=${s}, expectPid=${t?.pid})`)}tryRecoverSessionIdConflict(){if(!this.sessionIdConflictDetected||this.stopped||!this.activeEvent||this.isEventAnswered())return!1;const e=this.activeEvent.eventId;if(this.sessionIdConflictRetriedEventIds.has(e))return this.sessionIdConflictDetected=!1,!1;this.sessionIdConflictRetriedEventIds.add(e),this.sessionIdConflictDetected=!1;const t=this.activeEvent.rawEvent;return a.warn("claude-adapter",`Detected Claude session-id conflict, auto-retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(t),!0}recoverMalformedToolCall(e){if(this.stopped||!this.activeEvent||this.isEventAnswered()||!this.claudeCliSessionId||!this.claudeSessionCwd)return"none";const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!t||t.stopReason!=="stop_sequence"||t.text.trim()!==ot)return"none";if(this.malformedToolRetriedEventIds.has(e))return a.warn("claude-adapter",`Malformed tool-call retry exhausted for ${e}, sending clean fallback`),"exhausted";this.malformedToolRetriedEventIds.add(e);const i={...this.activeEvent.rawEvent,content:lt};return a.warn("claude-adapter",`Detected malformed tool-call, silently retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(i),"retried"}async handleInternalInvoke(e,t,i){if(e==="event_tool_call"){const s=String(t.tool_name??""),n=ze(s,{...t.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const l=this.activeEvent,c=String(n.event_id??"").trim();if(c===""?n.event_id=l.eventId:(s==="grix_reply"||s==="grix_complete")&&c!==l.eventId&&(a.warn("claude-adapter",`${s}: event_id mismatch, correcting: supplied=${c}, active=${l.eventId}`),n.event_id=l.eventId),String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId),s==="grix_reply"&&String(n.quoted_message_id??"").trim()===""){const d=String(l.rawEvent?.msg_id??"").trim();d&&(n.quoted_message_id=d)}}else if(this.lastClearedEvent&&s==="grix_reply"){const l=this.lastClearedEvent;Date.now()-l.ts<mt&&(String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId),n.event_id="",a.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${l.eventId} (cleared ${(Date.now()-l.ts)/1e3}s ago)`))}s==="grix_reply"&&String(n.event_id??"").trim()!==""&&this.completedEventIds.has(String(n.event_id??"").trim())&&(a.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(n.event_id??"").trim()}`),n.event_id="");const o=Be(s,n);if(!o.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${o.error}`);if(r.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(s==="grix_access_control")return this.executeAccessControl(n);if(qe(s)){const l=this.activeEvent;l&&(l.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const c=Ue(r,s,n);if(c.isError)throw new Error(c.content[0]?.text??"event tool failed");if(i?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(s,n),JSON.parse(c.content[0]?.text??"null")}finally{if(l&&(l.toolCallInFlight=!1,l.pendingStopHook&&this.activeEvent===l)){const c=l.pendingStopHook;l.pendingStopHook=void 0,this.finalizeActiveEvent(c)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${s}`)}return this.bridgeCallbacks.agentInvoke(e,t)}getEventToolHandle(){const e=this;return{status:"ready",getStatusSnapshot:()=>({status:"ready",connectedAt:Date.now(),reconnectAttempts:0}),sendEventAck:t=>{e.bridgeCallbacks.sendEventAck(t.event_id,t.session_id??"")},sendStreamChunk:t=>{e.bridgeCallbacks.sendStreamChunk(t.event_id??"",t.session_id,t.delta_content??"",Number(t.chunk_seq??0)||1,t.is_finish===!0,t.client_msg_id,t.quoted_message_id)},sendMsg:t=>{if(typeof e.bridgeCallbacks.sendDirectMessage!="function"){a.warn("claude-adapter","sendDirectMessage callback not provided, dropping direct message");return}e.bridgeCallbacks.sendDirectMessage({sessionId:t.session_id,clientMsgId:t.client_msg_id,content:t.content,quotedMessageId:t.quoted_message_id})},sendEventResult:t=>{e.bridgeCallbacks.sendEventResult(t.event_id,t.status,t.msg,t.code)},sendSessionActivitySet:t=>{}}}postProcessEventToolCall(e,t){const i=String(t.event_id??"").trim();if(!i||this.activeEvent?.eventId!==i){a.warn("claude-adapter",`postProcessEventToolCall: event_id mismatch (tool=${e}, eventId=${i}, activeEventId=${this.activeEvent?.eventId??"none"})`);return}if(e==="grix_complete"){this.completedEventIds.set(i,Date.now()),this.clearActiveEvent();return}if(e==="grix_reply"){const s=!this.activeEvent.replied;this.activeEvent.replied=!0;const n=String(t.text??"");n.length>0&&(this.activeEvent.lastReplyText=n),this.markActiveEventActivity(i,String(t.session_id??"").trim()||void 0),s&&(this.activeEvent.repliedAt=Date.now(),this.startPostReplyDeadline(i),this.startPostReplyJsonlWatcher(i))}}async executeAccessControl(e){const t=String(e.action??""),i=Je[t];if(!i)throw new Error(`\u672A\u77E5 access_control action: ${t}`);const s={};return e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy),this.bridgeCallbacks.agentInvoke("claude_access_control",{verb:i,payload:s},3e4)}resolveSessionRuntime(){if(!this.runtimeResolver)return{};try{return this.runtimeResolver(this.sessionId)??{}}catch(e){throw new Error(`resolve session runtime failed: ${e instanceof Error?e.message:String(e)}`)}}async validatePluginDir(e){const t=String(e??"").trim();if(!t)return;let i;try{i=await ne(t)}catch{throw new Error(`pluginDir is not accessible: ${t}`)}if(!i.isDirectory())throw new Error(`pluginDir is not a directory: ${t}`);const s=h(t,".mcp.json");try{(await ne(s)).isFile()&&(await se(s),a.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=h(x(),".claude.json");try{const i=await E(t,"utf8"),s=JSON.parse(i);if(s.projects?.[e]?.hasTrustDialogAccepted===!0)return;s.projects||(s.projects={}),s.projects[e]||(s.projects[e]={}),s.projects[e].hasTrustDialogAccepted=!0,await C(t,JSON.stringify(s),"utf8"),a.info("claude-adapter",`Pre-trusted workspace: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${i}`)}}async ensureSkipDangerousPermissionPrompt(){const e=h(x(),".claude","settings.json");try{let t={};try{t=JSON.parse(await E(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await M(h(x(),".claude"),{recursive:!0}),await C(e,JSON.stringify(t,null,2),"utf8"),a.info("claude-adapter","Set skipDangerousModePermissionPrompt=true in user settings to skip BypassPermissions dialog")}catch(t){a.warn("claude-adapter",`Failed to set skipDangerousModePermissionPrompt: ${t}`)}}async isChannelGateClosed(){if(this.channelGateClosed)return!0;try{const e=await E(h(x(),".claude.json"),"utf8"),i=JSON.parse(e).cachedGrowthBookFeatures;return!i||Object.keys(i).length===0?!1:i.tengu_harbor!==!0}catch{return!1}}async ensureClaudeOnboardingFlags(e){const t=h(x(),".claude.json");try{let i;try{const r=await E(t,"utf8");i=JSON.parse(r)}catch{i={}}let s=!1;i.hasCompletedOnboarding||(i.hasCompletedOnboarding=!0,i.lastOnboardingVersion||(i.lastOnboardingVersion="2.1.31"),s=!0),i.projects||(i.projects={});const n=i.projects;if(n[e]||(n[e]={}),n[e].hasCompletedProjectOnboarding||(n[e].hasCompletedProjectOnboarding=!0,s=!0),!s)return;await C(t,JSON.stringify(i,null,2),"utf8"),a.info("claude-adapter",`Marked Claude onboarding complete: ${e}`)}catch(i){a.warn("claude-adapter",`Failed to mark Claude onboarding complete: ${i}`)}}async injectStatusLineSettings(e){try{const t=this.resolveProjectRoot(),i=h(t,"dist","scripts","status-line-forwarder.js"),s=h(e,".claude"),n=h(s,"settings.json");await M(s,{recursive:!0});let r={};try{r=JSON.parse(await E(n,"utf8"))}catch{}const l={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=l;const c=`${JSON.stringify(r,null,2)}
|
|
17
|
+
`;await C(n,c,"utf8"),a.info("claude-adapter",`Injected statusLine settings: ${n}`)}catch(t){a.warn("claude-adapter",`Failed to inject statusLine settings: ${t instanceof Error?t.message:t}`)}}async ensureUserMcpServer(e,t,i){const s=this.resolveServerEntryPath(t),n=process.execPath,r=[s],o=h(x(),".claude.json");let l=null;try{const p=await E(o,"utf8");l=JSON.parse(p)?.mcpServers?.[F]??null}catch{}if(l&&String(l.type||"stdio").trim()==="stdio"&&String(l.command??"").trim()===n&&Array.isArray(l.args)&&l.args.length===r.length&&l.args.every((p,w)=>p===r[w]))return;a.info("claude-adapter",`Registering user-scoped MCP server: ${F} -> ${n} ${r.join(" ")}`);try{j(`${e} mcp remove -s user ${F}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"})}catch{}const c=["mcp","add","--scope","user",F,"--",n,...r],d=process.platform==="win32"?'"':"'",f=j(`${e} ${c.map(p=>`${d}${p}${d}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe"});a.info("claude-adapter",`MCP server registered: ${f.trim()||"ok"}`)}resolveServerEntryPath(e){const t=h(e,"server","main.js");try{if(Q(t))return t}catch{}const i=h(e,"dist","index.js");try{if(Q(i))return i}catch{}throw new Error(`Cannot find grix-claude server entry in pluginDir: ${e}`)}clearActiveEvent(){const e=this.activeEvent;this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.clearActiveEventPostReplyTimer(),this.clearActiveEventPostReplyWatcher(),e&&(this.sessionIdConflictRetriedEventIds.delete(e.eventId),this.malformedToolRetriedEventIds.delete(e.eventId),this.completedEventIds.set(e.eventId,Date.now()),this.lastClearedEvent={eventId:e.eventId,sessionId:e.sessionId,ts:Date.now()},this.emit(`reply:${e.sessionId}`,{status:"completed"}),this.emit("eventDone",e.eventId)),this.activeEvent=null,this.stopComposing()}startComposing(e,t){}stopComposing(){}clearComposingTimer(){}resolveHookSignalsPath(){const e=_();return h(e.dataDir,`hook-signals-${this.sessionId}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=_().hookSignalsLogPath,i=new je(e,t);a.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new Ne({hookSignalStore:i,onActivity:s=>this.onHookActivity(s),onStop:()=>this.onClaudeTurnEnd("Stop"),onStopFailure:()=>this.onClaudeTurnEnd("StopFailure"),onCompactStart:()=>this.beginCompaction(),onCompactResult:()=>this.finishCompaction("post-compact-hook"),onPermissionRequest:(s,n)=>this.handlePermissionHookEvent(s,n)}),this.activityManager.start()}onClaudeTurnEnd(e){if(a.info("claude-adapter",`Hook activity: ${e}`),!this.activeEvent){this.stopHookBarrierSessionId&&(a.info("claude-adapter",`Late Stop hook after end_turn finalize \u2014 releasing barrier (session=${this.stopHookBarrierSessionId})`),this.clearStopHookBarrier());return}if(this.activeEvent.toolCallInFlight){a.info("claude-adapter",`Stop hook deferred: toolCallInFlight for ${this.activeEvent.eventId}`),this.activeEvent.pendingStopHook=e;return}this.finalizeActiveEvent(e)}forwardIncrementalText(e){const t=this.activeEvent;if(!t||t.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const i=t.textForwardOffset??t.jsonlBaseOffset??0,{segments:s,newOffset:n}=Ye(this.claudeCliSessionId,this.claudeSessionCwd,i);if(t.textForwardOffset=n,s.length===0)return;const r=t.sessionId;for(const o of s){const l=o.trim();if(!l)continue;const c=`txt_${e}_${this.textForwardSeq++}`;this.bridgeCallbacks.sendStreamChunk(e,r,l,1,!1,c),this.bridgeCallbacks.sendStreamChunk(e,r,"",2,!0,c),t.textForwarded||(t.textForwarded=!0,t.repliedAt===void 0&&(t.repliedAt=Date.now()),this.startPostReplyDeadline(e),this.startPostReplyJsonlWatcher(e))}this.markActiveEventActivity(e,r),a.info("claude-adapter",`Forwarded ${s.length} plain-text segment(s) for ${e}`)}flushForwardedTail(e){this.activeEvent?.eventId===e&&this.activeEvent.textForwarded&&this.forwardIncrementalText(e)}isEventAnswered(e=this.activeEvent){return!!(e&&(e.replied||e.textForwarded))}attemptRescueFromJsonl(e,t){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return a.info("claude-adapter",`Rescue skipped: no claudeCliSessionId or claudeSessionCwd for ${e}`),!1;const i=this.activeEvent?.jsonlBaseOffset,s=Ve(this.claudeCliSessionId,this.claudeSessionCwd,i);if(!s)return a.info("claude-adapter",`Rescue failed: no assistant text found in JSONL for ${e}`),!1;const n=`rescue_${e}_${Date.now()}`;return this.bridgeCallbacks.sendStreamChunk(e,t,s,1,!1,n),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,n),a.info("claude-adapter",`Rescue succeeded for event ${e}: sent ${s.length} chars from JSONL`),!0}warnUnsentFinalIfAny(e){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=this.activeEvent?.jsonlBaseOffset,i=B(this.claudeCliSessionId,this.claudeSessionCwd,t);if(!i||i.stopReason!=="end_turn")return;const s=i.text?.trim()??"";if(!s)return;const n=(this.activeEvent?.lastReplyText??"").trim();n&&(s===n||n.includes(s))||a.warn("claude-adapter",`Unsent final end_turn text for ${e} (${s.length} chars) not delivered \u2014 \u5DF2\u5E94\u7B54\u8FC7\uFF0C\u6309\u7B56\u7565\u4E0D\u8865\u53D1\uFF08\u7591\u4F3C\u56DE\u5408\u6536\u5C3E\u81EA\u8FF0\uFF1B\u82E5\u786E\u4E3A\u6F0F\u53D1\u7ED3\u8BBA\u53EF\u636E\u6B64\u6392\u67E5\uFF09`)}finalizeActiveEvent(e){if(!this.activeEvent)return;const t=this.activeEvent.eventId,i=this.activeEvent.sessionId;if(this.isEventAnswered()&&this.activeEvent.jsonlBaseOffset!==void 0&&this.claudeCliSessionId&&this.claudeSessionCwd){const n=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!n||n.stopReason!=="end_turn")if(oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset))a.info("claude-adapter",`Stop hook: terminal tool_result detected for ${t} (grix_reply as final action, no end_turn expected), finalizing as responded`);else{a.info("claude-adapter",`Stop hook suppressed for ${t}: no end_turn in JSONL after offset=${this.activeEvent.jsonlBaseOffset} \u2014 likely resume-drain hook, waiting for JSONL watcher`),this.markActiveEventActivity(t,i);return}}this.flushForwardedTail(t);let s=!1;if(!this.activeEvent.apiFormatError)if(this.activeEvent.replied||this.activeEvent.textForwarded)this.activeEvent.replied&&!this.activeEvent.textForwarded&&this.warnUnsentFinalIfAny(t);else{const n=this.recoverMalformedToolCall(t);if(n==="retried")return;n==="exhausted"?(this.bridgeCallbacks.sendStreamChunk(t,i,ct,1,!1,`mtc_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`mtc_${t}`),s=!0):s=this.attemptRescueFromJsonl(t,i)}if(this.activeEvent.replied||this.activeEvent.textForwarded||s)a.info("claude-adapter",`Stop hook received, finalizing event ${t} as responded (replied=${this.activeEvent.replied}, textForwarded=${this.activeEvent.textForwarded}, rescued=${s})`),this.bridgeCallbacks.sendEventResult(t,"responded");else{a.warn("claude-adapter",`Active event not confirmed when ${e}: ${t} apiFormatError=${!!this.activeEvent.apiFormatError}, sending failed result`);const n=this.activeEvent.apiFormatError?"Claude hit an API format error. Please start a new conversation (/grix open <dir>).":"Claude exited before completing its reply. Please try again.";this.bridgeCallbacks.sendStreamChunk(t,i,n,1,!1,`err_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`err_${t}`),this.bridgeCallbacks.sendEventResult(t,"failed",n,"agent_stop_failure")}this.clearActiveEvent(),this.armStopHookBarrier(i)}onHookActivity(e){const t=this.activeEvent?.sessionId;if(a.info("claude-adapter",`Hook activity: tool=${e?.tool_name??"(clear)"} session=${t??"(none)"}`),e&&this.activeEvent&&(this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId),this.forwardIncrementalText(this.activeEvent.eventId),(this.activeEvent.replied||this.activeEvent.textForwarded)&&this.startPostReplyDeadline(this.activeEvent.eventId)),e&&!this.activeEvent&&!this.compacting&&this.noteSelfDrivenActivity(),!!t)if(e){this.startComposing(t,e);const i=this.activeEvent;if(i){const s=e.event_name,n=e.tool_name,r=e.tool_input??"",o=n?Ge(n):!1;if(n==="ExitPlanMode"&&s==="PreToolUse")a.info("claude-adapter","ExitPlanMode detected; waiting for user decision via permission card");else if(n==="AskUserQuestion"&&s==="PreToolUse")this.handleAskUserQuestion(i,r);else if(s==="PreToolUse"&&n)r&&(this.lastPreToolInput=r),o||this.bridgeCallbacks.sendToolUse(i.eventId,i.sessionId,n,r);else if((s==="PostToolUse"||s==="PostToolUseFailure")&&n){if(n==="AskUserQuestion")return;const l=r||this.lastPreToolInput;if(this.lastPreToolInput="",!o){const c=s==="PostToolUseFailure"?`(failed) ${l}`:l;this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,c)}}}}else this.startComposing(t)}handlePermissionHookEvent(e,t){if(!this.activeEvent){a.warn("claude-adapter","PermissionRequest without active event, ignoring");return}if(e==="AskUserQuestion"){a.info("claude-adapter","Skip permission card for AskUserQuestion; handled by agent_question card flow");return}const i=this.activeEvent,s=_();new q(s.permissionRequestsDir).listPending().then(r=>{const o=r.length>0?r[r.length-1]:null;if(!o){a.warn("claude-adapter","No pending permission request found in store");return}const l=o.request_id;this.pendingPermissions.set(l,{eventId:i.eventId,sessionId:i.sessionId});const c=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const p=tt(c);p&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,p)}const d=e==="ExitPlanMode"?"":c.slice(0,100),f=d?`${e}: ${d}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:l,toolName:e,toolTitle:f}),a.info("claude-adapter",`Sent permission card: approvalId=${l} tool=${e}`)}).catch(r=>{a.warn("claude-adapter",`Failed to send permission card: ${r}`)})}handleAskUserQuestion(e,t){const i=ce(t);if(!i){a.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=_(),n=new ae(s.questionRequestsDir);n.listPending().then(async r=>{const o=[...r].reverse().filter(d=>String(d.session_id??"").trim()===this.claudeCliSessionId),l=o.find(d=>String(d.event_id??"").trim()===e.eventId)??o.find(d=>String(d.event_id??"").trim()==="")??o[0]??null,c=l?.request_id??`fallback-${Date.now()}`;l&&String(l.event_id??"").trim()===""&&await n.updateRequest(l.request_id,{event_id:e.eventId}),this.pendingQuestion.set(c,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0,this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:c,mode:"form",questions:i}),a.info("claude-adapter",`Sent agent_question card: request_id=${c} questions=${i.length}`)}).catch(()=>{a.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return ce(e)}parseAskUserQuestionInput(e){let t;try{t=JSON.parse(e)}catch{return null}const i=[],s=t.questions;if(!Array.isArray(s)||s.length===0)return null;for(let n=0;n<s.length;n++){const r=s[n];if(!r||typeof r!="object")continue;const o=String(r.header??r.question??`Question ${n+1}`),l=String(r.prompt??""),c=Array.isArray(r.options)?r.options:void 0,d=r.multiSelect===!0;i.push({header:o,prompt:l,...c&&c.length>0?{options:c}:{},...d?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-rt;for(const[t,i]of this.completedEventIds.entries())i<e&&this.completedEventIds.delete(t)}markActiveEventActivity(e,t){const i=this.activeEvent;i&&(e&&i.eventId!==e||t&&i.sessionId!==t||(this.resetActiveEventIdleTimer(i.eventId),this.isEventAnswered(i)||this.resetActiveEventHardTimer(i.eventId)))}clearActiveEventIdleTimer(){this.activeEventIdleTimer&&(clearTimeout(this.activeEventIdleTimer),this.activeEventIdleTimer=null)}clearActiveEventHardTimer(){this.activeEventHardTimer&&(clearTimeout(this.activeEventHardTimer),this.activeEventHardTimer=null)}shouldExtendByLiveness(e){const t=this.activeEvent;if(!t||t.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd||!this.claudeProcess&&!this.claudePty)return!1;if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<fe)return t.livenessExtendStartAt=Date.now(),!0;const i=le(this.claudeCliSessionId,this.claudeSessionCwd,t.jsonlBaseOffset);if(i.freshMs===null||!(t.jsonlBaseOffset===void 0?i.freshMs<X:i.lastStopReason!==null?i.lastStopReason!=="end_turn":i.freshMs<X))return!1;if(i.freshMs<X)t.livenessExtendStartAt=Date.now();else{const n=t.livenessExtendStartAt??Date.now();if(t.livenessExtendStartAt===void 0&&(t.livenessExtendStartAt=n),Date.now()-n>pe)return a.warn("claude-adapter",`Liveness extension budget exhausted for ${e} (no JSONL writes for ${Math.round((Date.now()-n)/6e4)}min), allowing close`),!1}return a.info("claude-adapter",`Liveness check: turn in progress for ${e} (lastStopReason=${i.lastStopReason??"none"}, freshMs=${i.freshMs}), extending`),!0}captureEventJsonlBaseOffset(e){const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=Y(this.claudeCliSessionId,this.claudeSessionCwd);t.jsonlBaseOffset=D(i)?L(i).size:0}catch{t.jsonlBaseOffset=0}}noteSelfDrivenActivity(){!this.selfDrivenActive&&this.lastClearedEvent&&Date.now()-this.lastClearedEvent.ts<pt||(this.selfDrivenLastSignalAt=Date.now(),!this.selfDrivenActive&&(this.selfDrivenActive=!0,a.info("claude-adapter",`Self-driven activity detected for session ${this.sessionId} (no active event), debouncing before reporting working state`),this.selfDrivenReportTimer=setTimeout(()=>{this.selfDrivenReportTimer=null,!(!this.selfDrivenActive||this.stopped||this.activeEvent)&&(this.selfDrivenReported=!0,a.info("claude-adapter",`Self-driven working state confirmed for session ${this.sessionId} (sustained >${me/1e3}s), reporting`),this.emit("sessionActivity",this.sessionId??"",!0))},me),this.selfDrivenReportTimer.unref(),this.selfDrivenSweepTimer=setInterval(()=>this.sweepSelfDriven(),ht),this.selfDrivenSweepTimer.unref()))}sweepSelfDriven(){if(!this.selfDrivenActive)return;if(this.stopped||this.activeEvent){this.stopSelfDriven();return}const e=Date.now()-this.selfDrivenLastSignalAt;if(e<ut){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0);return}if(this.claudeCliSessionId&&this.claudeSessionCwd){const t=le(this.claudeCliSessionId,this.claudeSessionCwd),i=t.lastStopReason!==null&&t.lastStopReason!=="end_turn",s=t.freshMs!==null&&t.freshMs<pe;if(i&&s){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0);return}}a.info("claude-adapter",`Self-driven activity ended for session ${this.sessionId} (quiet for ${Math.round(e/1e3)}s)`),this.stopSelfDriven()}stopSelfDriven(){this.selfDrivenSweepTimer&&(clearInterval(this.selfDrivenSweepTimer),this.selfDrivenSweepTimer=null),this.selfDrivenReportTimer&&(clearTimeout(this.selfDrivenReportTimer),this.selfDrivenReportTimer=null),this.selfDrivenActive&&(this.selfDrivenActive=!1,this.selfDrivenReported&&(this.selfDrivenReported=!1,this.emit("sessionActivity",this.sessionId??"",!1)))}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.activeEvent?.toolCallInFlight){a.info("claude-adapter",`Idle timeout skipped: toolCallInFlight for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.activeEvent?.awaitingUserQuestion){a.info("claude-adapter",`Idle timeout skipped: awaitingUserQuestion for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.pendingPermissions.size>0){a.info("claude-adapter",`Idle timeout skipped: pendingPermissions=${this.pendingPermissions.size} for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.shouldExtendByLiveness(e)){this.resetActiveEventIdleTimer(e);return}this.finalizeStuckActiveEvent(e,"idle")}},ue)}finalizeStuckActiveEvent(e,t){const i=this.isEventAnswered(),s=i?"responded":"failed",n=t==="idle"?ue:he,r=i?void 0:t==="idle"?`agent idle for ${n/1e3}s`:`agent exceeded max duration ${n/1e3}s`,o=i?void 0:t==="idle"?"agent_idle_timeout":"agent_hard_timeout";a.error("claude-adapter",`Active event ${t} timeout (${n/1e3}s): ${e}, answered=${i}, sending ${s}`),this.flushForwardedTail(e),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,s,r,o),this.clearActiveEvent(),s==="failed"&&this.emit("stuck")}resetActiveEventHardTimer(e){this.clearActiveEventHardTimer(),this.activeEventHardTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.shouldExtendByLiveness(e)){this.resetActiveEventHardTimer(e);return}this.finalizeStuckActiveEvent(e,"hard")}},he)}clearActiveEventPostReplyTimer(){this.activeEventPostReplyTimer&&(clearTimeout(this.activeEventPostReplyTimer),this.activeEventPostReplyTimer=null)}clearActiveEventPostReplyWatcher(){if(this.activeEventPostReplyWatcher){try{this.activeEventPostReplyWatcher.close()}catch{}this.activeEventPostReplyWatcher=null}this.activeEventPostReplyPoll&&(clearInterval(this.activeEventPostReplyPoll),this.activeEventPostReplyPoll=null)}startPostReplyJsonlWatcher(e){if(this.clearActiveEventPostReplyWatcher(),!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=Y(this.claudeCliSessionId,this.claudeSessionCwd);if(!this.activeEvent?.sessionId)return;if(!D(t)){a.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=L(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;try{this.activeEventPostReplyWatcher=Me(t,()=>{n&&clearTimeout(n),n=setTimeout(()=>this.finalizeIfEndTurn(e),300)}),this.activeEventPostReplyPoll=setInterval(()=>this.finalizeIfEndTurn(e),dt),a.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(r){a.warn("claude-adapter",`JSONL watcher start failed for ${e}: ${r}`)}}finalizeIfEndTurn(e){if(this.stopped||this.activeEvent?.eventId!==e||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=B(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(t&&t.stopReason==="end_turn"){a.info("claude-adapter",`JSONL watcher: end_turn detected for ${e}, finalizing as responded`);const i=this.activeEvent.sessionId;this.flushForwardedTail(e),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent(),this.armStopHookBarrier(i);return}if(this.activeEvent?.replied&&this.activeEvent.jsonlBaseOffset!==void 0&&oe(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset,!0)){if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<fe)return;a.info("claude-adapter",`JSONL poll: terminal tool_result detected for ${e} (file quiet, no end_turn), finalizing as responded`),this.flushForwardedTail(e),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()}}startPostReplyDeadline(e){this.clearActiveEventPostReplyTimer(),this.clearActiveEventHardTimer(),this.activeEventPostReplyTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e||!this.activeEvent?.replied&&!this.activeEvent?.textForwarded)return;if(this.shouldExtendByLiveness(e)){this.startPostReplyDeadline(e);return}const t=this.activeEvent.repliedAt,i=Math.round((Date.now()-t)/1e3);a.info("claude-adapter",`Post-reply deadline reached for ${e} (${i}s since reply), completing as responded`),this.flushForwardedTail(e),this.bridgeCallbacks.sendEventResult(e,"responded",void 0,void 0),this.clearActiveEvent()},at)}beginCompaction(){this.compacting||(this.compacting=!0,a.info("claude-adapter","Compaction started; gating input"),this.emit("pauseIntake","compaction"),this.resetCompactingActivityTimer())}resetCompactingActivityTimer(){this.compactingTimer&&clearTimeout(this.compactingTimer),this.compactingTimer=setTimeout(()=>{a.warn("claude-adapter","Compaction stall: no PTY activity for 90s, emitting stuck"),this.emit("stuck")},9e4),this.compactingTimer.unref?.()}finishCompaction(e){if(!this.compacting)return;this.compacting=!1,this.compactingTimer&&(clearTimeout(this.compactingTimer),this.compactingTimer=null),a.info("claude-adapter",`Compaction finished (${e}); resuming intake`),this.emit("resumeIntake","compaction");const t=this.compactionDoneResolver;this.compactionDoneResolver=null,t?.(e)}armStopHookBarrier(e){this.stopHookBarrierTimer&&clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierSessionId=e,this.emit("pauseIntake","barrier"),this.stopHookBarrierTimer=setTimeout(()=>{this.stopHookBarrierSessionId===e&&(a.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier())},vt)}clearStopHookBarrier(){const e=this.stopHookBarrierSessionId!==null;this.stopHookBarrierSessionId=null,this.stopHookBarrierTimer&&(clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierTimer=null),e&&this.emit("resumeIntake","barrier")}clearPendingMcpFailureTimer(){this.pendingMcpFailureTimer&&(clearTimeout(this.pendingMcpFailureTimer),this.pendingMcpFailureTimer=null)}markMcpStartupFailure(){if(!this.mcpStartupFailureHandled&&(this.mcpStartupFailureHandled=!0,this.mcpChannelBroken=!0,a.error("claude-adapter","Claude reported blocking MCP server startup failure"),this.activeEvent)){const e=this.activeEvent.eventId,t=this.activeEvent.sessionId;this.bridgeCallbacks.sendStreamChunk(e,t,`
|
|
18
18
|
|
|
19
|
-
Error: MCP server startup failed`,1,!1),this.bridgeCallbacks.sendEventResult(e,"failed","MCP server startup failed"),this.completedEventIds.set(e,Date.now()),this.clearActiveEvent()}}}class _t extends re{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){a.warn("claude-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}function y(
|
|
20
|
-
`),"utf8"),{expectPath:i,pidPath:s}}async function Pt(
|
|
21
|
-
`;let r="";try{r=await E(
|
|
22
|
-
`;let i="";try{i=await E(
|
|
23
|
-
`)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){a.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${
|
|
19
|
+
Error: MCP server startup failed`,1,!1),this.bridgeCallbacks.sendEventResult(e,"failed","MCP server startup failed"),this.completedEventIds.set(e,Date.now()),this.clearActiveEvent()}}}class _t extends re{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){a.warn("claude-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}function y(u){if(u==null)return 0;const e=Number(u);return Number.isFinite(e)?e:0}function G(u){return String(u).replace(/\\/g,"\\\\").replace(/\{/g,"\\{").replace(/\}/g,"\\}")}async function Et(u,e,t){const i=h(u,"claude.expect"),s=h(u,"claude.pid"),n=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set cmd_fifo {${G(h(u,"cmd.fifo"))}}`,"file delete -force $cmd_fifo","exec mkfifo $cmd_fifo","set cmd_fd [open $cmd_fifo r+]","fconfigure $cmd_fd -blocking 0 -buffering line",`set claude_command [list {${G(e)}}${t.map(r=>` {${G(r)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${G(s)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","after 500",'send -- "\\r"',"expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(I am using this for local development|Please use --channels|dangerously-load-development-channels)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter|Continue.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {(?i)Listening.*channel messages.*server:grix} {"," set startup_prompt_armed 0"," }"," eof {}","}","proc handle_cmd {} {"," upvar cmd_fd cmd_fd cmd_fifo cmd_fifo"," if {[catch {gets $cmd_fd} __line]} { return }"," if {[eof $cmd_fd]} {"," catch {close $cmd_fd}"," if {[catch {set cmd_fd [open $cmd_fifo r+]} err]} { return }"," fconfigure $cmd_fd -blocking 0 -buffering line"," fileevent $cmd_fd readable handle_cmd",' } elseif {$__line ne ""} {',' send -- "$__line\\r"'," }","}","fileevent $cmd_fd readable handle_cmd","expect_background {"," -re .+ { }"," eof { set ::__done 1 }","}","set ::__done 0","vwait ::__done",""];return await C(s,"","utf8"),await C(i,n.join(`
|
|
20
|
+
`),"utf8"),{expectPath:i,pidPath:s}}async function Pt(u,e=50,t=100){for(let i=0;i<e;i++){try{const s=await E(u,"utf8"),n=parseInt(String(s).trim(),10);if(Number.isFinite(n)&&n>0)return n}catch{}await new Promise(s=>setTimeout(s,t))}return 0}function Ct(u,e){return new Promise((t,i)=>{let s=!1;const n=o=>{s||(s=!0,u.off("error",r),o())},r=o=>n(()=>i(o));u.once("error",r),Pt(e).then(o=>n(()=>t(o))).catch(o=>n(()=>i(o)))})}function Se(u){return u.replace(/[/\\]/g,"-")}function kt(u){const e=_();return h(e.dataDir,"claude-mcp-configs",`${Se(u)}.json`)}function At(u){const e=_();return h(e.dataDir,"claude-system-prompts",`${Se(u)}.txt`)}async function It(u,e,t){await M(h(_().dataDir,"claude-mcp-configs"),{recursive:!0});const i={[F]:{type:"stdio",command:process.execPath,args:e}};t&&(i["grix-app-bridge"]={type:"stdio",command:process.execPath,args:t});const n=`${JSON.stringify({mcpServers:i},null,2)}
|
|
21
|
+
`;let r="";try{r=await E(u,"utf8")}catch{}r!==n&&(await C(u,n,"utf8"),a.info("claude-adapter",`Wrote MCP config: ${u}`))}async function bt(u,e){await M(h(_().dataDir,"claude-system-prompts"),{recursive:!0});const t=`${e}
|
|
22
|
+
`;let i="";try{i=await E(u,"utf8")}catch{}i!==t&&await C(u,t,"utf8")}function ee(u,e){if(process.platform==="win32")return;let t=!1;try{const i=j(`ps ax -o pid,command | grep -E -- '--(session-id|resume) ${u}' | grep -v grep`,{encoding:"utf8",timeout:5e3}).trim();if(i)for(const s of i.split(`
|
|
23
|
+
`)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){a.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${u}`);try{process.kill(n,"SIGTERM"),t=!0}catch{}}}}catch{}t||z(u)}function z(u){const e=h(x(),".claude"),t=[h(e,"session-env",u)];try{const i=h(e,"sessions");for(const s of be(i))if(s.endsWith(".json"))try{const n=h(i,s),r=JSON.parse(Q(n,"utf8"));r&&r.sessionId===u&&t.push(n)}catch{}}catch{}for(const i of t)try{L(i),$e(i,{recursive:!0,force:!0}),a.info("claude-adapter",`Removed stale session file: ${i}`)}catch{}}function _e(u,e){if(!e)return!1;const t=h(x(),".claude"),i=$t(e),s=h(t,"projects",i,`${u}.jsonl`);try{return L(s),!0}catch{return!1}}function Tt(){return process.env.ComSpec||h(process.env.SystemRoot||"C:\\Windows","System32","cmd.exe")}function $t(u){const e=u.replace(/\\/g,"/").replace(/:\/\//g,"/");if(/^[a-zA-Z]:\//.test(e)||e.startsWith("//"))return e.replace(/[^a-zA-Z0-9]/g,"-");let i=e;try{i=Te(e)}catch{}return i.replace(/[^a-zA-Z0-9]/g,"-")}export{Ee as ClaudeAdapter,$t as deriveClaudeProjectDirName,ce as parseAskUserQuestionPayload,Tt as resolveWindowsShellCommand};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const
|
|
1
|
+
import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const r of t)e.push(r);const n=Buffer.concat(e).toString("utf8").trim();return n?JSON.parse(n):{}}function k(t){const e=(t.headers.authorization??"").trim();return e.toLowerCase().startsWith("bearer ")?e.slice(7).trim():""}class w{host="127.0.0.1";port=0;token;callbacks;server=null;address=null;constructor(e){this.token=d(),this.callbacks=e}getToken(){return this.token}getURL(){return this.address?`http://${this.address.address}:${this.address.port}`:""}async start(){this.server||(this.server=c.createServer(async(e,n)=>{try{await this.handleRequest(e,n)}catch(r){h(n,r instanceof Error?r.message:String(r))}}),await new Promise((e,n)=>{this.server.once("error",n),this.server.listen(this.port,this.host,()=>{this.server.off("error",n),e()})}),this.address=this.server.address(),o.info("claude-bridge",`Bridge server listening on ${this.getURL()}`))}async stop(){if(!this.server)return;const e=this.server;this.server=null,this.address=null,e.closeIdleConnections?.(),e.closeAllConnections?.(),await new Promise((n,r)=>{e.close(s=>s?r(s):n())})}async handleRequest(e,n){if(k(e)!==this.token){l(n);return}if(e.method!=="POST"){n.writeHead(405,{"content-type":"application/json"}),n.end(JSON.stringify({error:"method_not_allowed"}));return}const r=new URL(e.url,"http://localhost").pathname,s=await v(e),i=f.get(r);if(!i){u(n);return}const a=await i(this.callbacks,s);p(n,a??{ok:!0})}}const f=new Map([["/v1/worker/register",async(t,e)=>(o.info("claude-bridge",`Worker registered: ${e.worker_id} (pid=${e.pid})`),t.onRegisterWorker(e))],["/v1/worker/status",async(t,e)=>(o.info("claude-bridge",`Worker status: ${e.status}`),t.onStatusUpdate(e))],["/v1/worker/send-text",async(t,e)=>t.onSendText(e)],["/v1/worker/send-stream-chunk",async(t,e)=>t.onSendStreamChunk(e)],["/v1/worker/send-media",async(t,e)=>t.onSendMedia(e)],["/v1/worker/delete-message",async(t,e)=>t.onDeleteMessage(e)],["/v1/worker/ack-event",async(t,e)=>t.onAckEvent(e)],["/v1/worker/event-result",async(t,e)=>t.onSendEventResult(e)],["/v1/worker/event-stop-ack",async(t,e)=>t.onSendEventStopAck(e)],["/v1/worker/event-stop-result",async(t,e)=>t.onSendEventStopResult(e)],["/v1/worker/session-composing",async(t,e)=>t.onSetSessionComposing(e)],["/v1/worker/agent-invoke",async(t,e)=>t.onAgentInvoke(e)],["/v1/worker/local-action-result",async(t,e)=>t.onLocalActionResult(e)]]);export{w as ClaudeBridgeServer};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as x}from"node:crypto";import{CallToolRequestSchema as S,ListToolsRequestSchema as w}from"@modelcontextprotocol/sdk/types.js";import{log as f}from"../../core/log/index.js";import{toolCallToInvoke as k}from"../../core/mcp/tools.js";const E=new Set(["contact_search","session_search","message_history","message_search","group_create","group_detail_read","group_leave_self","group_member_add","group_member_remove","group_member_role_update","group_all_members_muted_update","group_member_speaking_update","group_dissolve","send_msg","delete_msg","agent_api_create","agent_category_list","agent_category_create","agent_category_update","agent_category_assign","agent_api_key_rotate"]),y=3e4,I=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},files:{type:"array",items:{type:"string"},description:"Optional absolute local file paths. Each file is uploaded through Agent API OSS presign before sending."},final:{type:"boolean",description:"Whether this is the final reply for the event. Defaults to false \u2014 the event stays open while Claude continues working, and auto-completes after inactivity. Set true only when this is definitively the last message for the event."}},required:["chat_id","event_id"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},code:{type:"string"},msg:{type:"string"}},required:["event_id","status"]}},{name:"delete_message",description:"Delete a previously sent message in the same grix-claude chat.",inputSchema:{type:"object",properties:{chat_id:{type:"string"},message_id:{type:"string"}},required:["chat_id","message_id"]}},{name:"status",description:"Show grix-claude runtime status, upstream access state, bridge health, and startup hints.",inputSchema:{type:"object",properties:{}}},{name:"send",description:"Send a message to a chat session proactively, without requiring an inbound event. Use for notifications or scheduled reports.",inputSchema:{type:"object",properties:{chat_id:{type:"string",description:"The target chat/session id."},text:{type:"string",description:"The message text to send."}},required:["chat_id","text"]}},{name:"access_pair",description:"Forward a sender pairing approval code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"access_deny",description:"Forward a sender pairing denial code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"allow_sender",description:"Ask upstream access control to allow a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"remove_sender",description:"Ask upstream access control to remove a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"access_policy",description:"Ask upstream access control to update the sender access policy.",inputSchema:{type:"object",properties:{policy:{type:"string",enum:["allowlist","open","disabled"]}},required:["policy"]}},{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},keyword:{type:"string"},id:{type:"string"},sessionId:{type:"string"},limit:{type:"number"},offset:{type:"number"},beforeId:{type:"string"}},required:["action"]}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string"},memberIds:{type:"array",items:{type:"string"}},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer",description:"Member type (for update_member_role / update_member_speaking)."},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted (for update_member_speaking)."}},required:["action"]}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},content:{type:"string"},msgType:{type:"number"},quotedMessageId:{type:"string"},threadId:{type:"string"}},required:["sessionId","content"]}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},msgId:{type:"string"}},required:["sessionId","msgId"]}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentId:{type:"string"},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"number"}},required:["action"]}}];function q(n,e){n.setRequestHandler(w,async()=>({tools:I})),n.setRequestHandler(S,async i=>{const{name:r,arguments:t}=i.params,s=t??{};try{switch(r){case"reply":return await A(s,e);case"complete":return await $(s,e);case"delete_message":return await T(s,e);case"status":return j(e);case"send":return await M(s,e);case"access_pair":case"access_deny":case"allow_sender":case"remove_sender":case"access_policy":return await R(r,s,e);case"grix_query":case"grix_group":case"grix_message_send":case"grix_message_unsend":case"grix_admin":return await O(r,s,e);default:return{content:[{type:"text",text:`Unknown tool: ${r}`}],isError:!0}}}catch(a){return f.error("claude-tools",`Tool ${r} error: ${a}`),{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}})}async function A(n,e){const i=e.getActiveEvent();if(!i)return{content:[{type:"text",text:"No active event to reply to"}],isError:!0};const r=String(n.text??""),t=String(n.chat_id??""),s=String(n.event_id??i.eventId),a=n.reply_to,d=n.files,_=n.final===!0;if(!t||!s)return{content:[{type:"text",text:"reply requires chat_id and event_id"}],isError:!0};if(!r.trim()&&(!d||d.length===0))return{content:[{type:"text",text:"reply requires at least one of text or files"}],isError:!0};const{text:g,quotedMessageId:v}=e.resolveQuotedMessageId(a,r),b=[];let m=0;const h=`reply_${s}_${Date.now()}`;try{if(g){const p=e.splitText(g);for(let o=0;o<p.length;o++){if(!e.isEventActive(s))return c("ignored: event no longer active");m++,e.bridge.sendStreamChunk(s,t,p[o],++i.chunkSeq,!1,h)}}if(d&&d.length>0)for(const p of d){if(!e.isEventActive(s))return c("ignored: event no longer active");m++;const o=await e.uploadFile(p,t),l=`${x()}_${m}`;e.bridge.sendMedia(s,t,o.access_url,o.file_name,v,l,o.extra),f.info("claude-tools",`File sent: ${o.file_name}`)}e.bridge.sendStreamChunk(s,t,"",++i.chunkSeq,!0,h)}catch(p){if(g&&b.length===0)try{const o=`fallback_${s}_${Date.now()}`,l=e.splitText(g);for(let u=0;u<l.length;u++)e.bridge.sendStreamChunk("",t,l[u],u+1,!1,o);return e.bridge.sendStreamChunk("",t,"",l.length+1,!0,o),e.markReplySent(s),_&&e.finalizeEvent(s,"responded"),c("sent via fallback")}catch{}if(!e.isEventActive(s))return c("ignored: event no longer active");throw e.bridge.sendEventResult(s,"failed",String(p),"send_msg_failed"),p}return e.markReplySent(s),_?e.finalizeEvent(s,"responded"):(i.responded=!0,e.clearActiveEvent("completed")),c("Reply sent")}async function $(n,e){const i=e.getActiveEvent(),r=String(n.event_id??""),t=n.status??"",s=n.code,a=n.msg;if(!r||!t)return{content:[{type:"text",text:"complete requires event_id and status"}],isError:!0};const d=["responded","canceled","failed"];return d.includes(t)?e.isEventActive(r)?(e.bridge.sendEventResult(r,t,a,s),e.clearActiveEvent(t),c("Event completed")):c("ignored: event no longer active"):{content:[{type:"text",text:`status must be one of: ${d.join(", ")}`}],isError:!0}}async function T(n,e){const i=String(n.chat_id??""),r=String(n.message_id??"");if(!i||!r)return{content:[{type:"text",text:"chat_id and message_id are required"}],isError:!0};try{return await e.bridge.agentInvoke("grix_message_unsend",{sessionId:i,msgId:r},y),c(`deleted (${r})`)}catch(t){return{content:[{type:"text",text:`Delete failed: ${t}`}],isError:!0}}}function j(n){const e=n.getStatusInfo();return{content:[{type:"text",text:JSON.stringify({alive:e.alive,active_event:e.activeEvent,pending_approvals:e.pendingPermissions,pending_questions:e.pendingElicitations})}]}}async function M(n,e){const i=String(n.chat_id??""),r=String(n.text??"");if(!i||!r)return{content:[{type:"text",text:"chat_id and text are required"}],isError:!0};try{const t=e.splitText(r),s=`send_${i}_${Date.now()}`;for(let a=0;a<t.length;a++)e.bridge.sendStreamChunk("",i,t[a],a+1,!1,s);return e.bridge.sendStreamChunk("",i,"",t.length+1,!0,s),c("sent")}catch(t){return{content:[{type:"text",text:`Send failed: ${t}`}],isError:!0}}}const C={access_pair:{verb:"pair_approve",payloadKey:"code"},access_deny:{verb:"pair_deny",payloadKey:"code"},allow_sender:{verb:"sender_allow",payloadKey:"sender_id"},remove_sender:{verb:"sender_remove",payloadKey:"sender_id"},access_policy:{verb:"policy_set",payloadKey:"policy"}};async function R(n,e,i){try{const r=C[n];if(!r)throw new Error(`Unknown access control tool: ${n}`);const t={};e.code!=null&&(t.code=e.code),e.sender_id!=null&&(t.sender_id=e.sender_id),e.policy!=null&&(t.policy=e.policy);const s=await i.bridge.agentInvoke("claude_access_control",{verb:r.verb,payload:t},y);return{content:[{type:"text",text:typeof s=="string"?s:JSON.stringify(s)}]}}catch(r){return{content:[{type:"text",text:`${n} failed: ${r}`}],isError:!0}}}async function O(n,e,i){try{const r=k(n,e);if(!E.has(r.action))throw new Error(`Action not allowed: ${r.action}`);const t=await i.bridge.agentInvoke(r.action,r.params,y);if(t&&Number(t.code??0)!==0)throw new Error(String(t.msg??"invoke failed"));return{content:[{type:"text",text:t?.data!=null?typeof t.data=="string"?t.data:JSON.stringify(t.data):JSON.stringify(t)}]}}catch(r){return{content:[{type:"text",text:`${n} failed: ${r}`}],isError:!0}}}function c(n){return{content:[{type:"text",text:n}]}}export{q as registerClaudeTools};
|
|
1
|
+
import{randomUUID as x}from"node:crypto";import{CallToolRequestSchema as S,ListToolsRequestSchema as w}from"@modelcontextprotocol/sdk/types.js";import{log as f}from"../../core/log/index.js";import{toolCallToInvoke as k}from"../../core/mcp/tools.js";const E=new Set(["contact_search","session_search","message_history","message_search","group_create","group_detail_read","group_leave_self","group_member_add","group_member_remove","group_member_role_update","group_all_members_muted_update","group_member_speaking_update","group_dissolve","send_msg","delete_msg","agent_api_create","agent_category_list","agent_category_create","agent_category_update","agent_category_assign","agent_api_key_rotate"]),y=3e4,I=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},files:{type:"array",items:{type:"string"},description:"Optional absolute local file paths. Each file is uploaded through Agent API OSS presign before sending."},final:{type:"boolean",description:"Whether this is the final reply for the event. Defaults to false \u2014 the event stays open while Claude continues working, and auto-completes after inactivity. Set true only when this is definitively the last message for the event."}},required:["chat_id","event_id"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},code:{type:"string"},msg:{type:"string"}},required:["event_id","status"]}},{name:"delete_message",description:"Delete a previously sent message in the same grix-claude chat.",inputSchema:{type:"object",properties:{chat_id:{type:"string"},message_id:{type:"string"}},required:["chat_id","message_id"]}},{name:"status",description:"Show grix-claude runtime status, upstream access state, bridge health, and startup hints.",inputSchema:{type:"object",properties:{}}},{name:"send",description:"Send a message to a chat session proactively, without requiring an inbound event. Use for notifications or scheduled reports.",inputSchema:{type:"object",properties:{chat_id:{type:"string",description:"The target chat/session id."},text:{type:"string",description:"The message text to send."}},required:["chat_id","text"]}},{name:"access_pair",description:"Forward a sender pairing approval code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"access_deny",description:"Forward a sender pairing denial code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"allow_sender",description:"Ask upstream access control to allow a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"remove_sender",description:"Ask upstream access control to remove a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"access_policy",description:"Ask upstream access control to update the sender access policy.",inputSchema:{type:"object",properties:{policy:{type:"string",enum:["allowlist","open","disabled"]}},required:["policy"]}},{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},keyword:{type:"string"},id:{type:"string"},sessionId:{type:"string"},limit:{type:"number"},offset:{type:"number"},beforeId:{type:"string"}},required:["action"]}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string"},memberIds:{type:"array",items:{type:"string"}},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer",description:"Member type (for update_member_role / update_member_speaking)."},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted (for update_member_speaking)."}},required:["action"]}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},content:{type:"string"},msgType:{type:"number"},quotedMessageId:{type:"string"},threadId:{type:"string"}},required:["sessionId","content"]}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},msgId:{type:"string"}},required:["sessionId","msgId"]}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentId:{type:"string"},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"number"}},required:["action"]}}];function q(r,e){r.setRequestHandler(w,async()=>({tools:I})),r.setRequestHandler(S,async i=>{const{name:n,arguments:t}=i.params,s=t??{};try{switch(n){case"reply":return await A(s,e);case"complete":return await $(s,e);case"delete_message":return await T(s,e);case"status":return j(e);case"send":return await M(s,e);case"access_pair":case"access_deny":case"allow_sender":case"remove_sender":case"access_policy":return await R(n,s,e);case"grix_query":case"grix_group":case"grix_message_send":case"grix_message_unsend":case"grix_admin":return await O(n,s,e);default:return{content:[{type:"text",text:`Unknown tool: ${n}`}],isError:!0}}}catch(a){return f.error("claude-tools",`Tool ${n} error: ${a}`),{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}})}async function A(r,e){const i=e.getActiveEvent();if(!i)return{content:[{type:"text",text:"No active event to reply to"}],isError:!0};const n=String(r.text??""),t=String(r.chat_id??""),s=String(r.event_id??i.eventId),a=r.reply_to,d=r.files,_=r.final===!0;if(!t||!s)return{content:[{type:"text",text:"reply requires chat_id and event_id"}],isError:!0};if(!n.trim()&&(!d||d.length===0))return{content:[{type:"text",text:"reply requires at least one of text or files"}],isError:!0};const{text:g,quotedMessageId:v}=e.resolveQuotedMessageId(a,n),b=[];let m=0;const h=`reply_${s}_${Date.now()}`;try{if(g){const p=e.splitText(g);for(let o=0;o<p.length;o++){if(!e.isEventActive(s))return c("ignored: event no longer active");m++,e.bridge.sendStreamChunk(s,t,p[o],++i.chunkSeq,!1,h)}}if(d&&d.length>0)for(const p of d){if(!e.isEventActive(s))return c("ignored: event no longer active");m++;const o=await e.uploadFile(p,t),l=`${x()}_${m}`;e.bridge.sendMedia(s,t,o.access_url,o.file_name,v,l,o.extra),f.info("claude-tools",`File sent: ${o.file_name}`)}e.bridge.sendStreamChunk(s,t,"",++i.chunkSeq,!0,h)}catch(p){if(g&&b.length===0)try{const o=`fallback_${s}_${Date.now()}`,l=e.splitText(g);for(let u=0;u<l.length;u++)e.bridge.sendStreamChunk("",t,l[u],u+1,!1,o);return e.bridge.sendStreamChunk("",t,"",l.length+1,!0,o),e.markReplySent(s),_&&e.finalizeEvent(s,"responded"),c("sent via fallback")}catch{}if(!e.isEventActive(s))return c("ignored: event no longer active");throw e.bridge.sendEventResult(s,"failed",String(p),"send_msg_failed"),p}return e.markReplySent(s),_?e.finalizeEvent(s,"responded"):(i.responded=!0,e.clearActiveEvent("completed")),c("Reply sent")}async function $(r,e){const i=e.getActiveEvent(),n=String(r.event_id??""),t=r.status??"",s=r.code,a=r.msg;if(!n||!t)return{content:[{type:"text",text:"complete requires event_id and status"}],isError:!0};const d=["responded","canceled","failed"];return d.includes(t)?e.isEventActive(n)?(e.bridge.sendEventResult(n,t,a,s),e.clearActiveEvent(t),c("Event completed")):c("ignored: event no longer active"):{content:[{type:"text",text:`status must be one of: ${d.join(", ")}`}],isError:!0}}async function T(r,e){const i=String(r.chat_id??""),n=String(r.message_id??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and message_id are required"}],isError:!0};try{return await e.bridge.agentInvoke("grix_message_unsend",{sessionId:i,msgId:n},y),c(`deleted (${n})`)}catch(t){return{content:[{type:"text",text:`Delete failed: ${t}`}],isError:!0}}}function j(r){const e=r.getStatusInfo();return{content:[{type:"text",text:JSON.stringify({alive:e.alive,active_event:e.activeEvent,pending_approvals:e.pendingPermissions,pending_questions:e.pendingElicitations})}]}}async function M(r,e){const i=String(r.chat_id??""),n=String(r.text??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and text are required"}],isError:!0};try{const t=e.splitText(n),s=`send_${i}_${Date.now()}`;for(let a=0;a<t.length;a++)e.bridge.sendStreamChunk("",i,t[a],a+1,!1,s);return e.bridge.sendStreamChunk("",i,"",t.length+1,!0,s),c("sent")}catch(t){return{content:[{type:"text",text:`Send failed: ${t}`}],isError:!0}}}const C={access_pair:{verb:"pair_approve",payloadKey:"code"},access_deny:{verb:"pair_deny",payloadKey:"code"},allow_sender:{verb:"sender_allow",payloadKey:"sender_id"},remove_sender:{verb:"sender_remove",payloadKey:"sender_id"},access_policy:{verb:"policy_set",payloadKey:"policy"}};async function R(r,e,i){try{const n=C[r];if(!n)throw new Error(`Unknown access control tool: ${r}`);const t={};e.code!=null&&(t.code=e.code),e.sender_id!=null&&(t.sender_id=e.sender_id),e.policy!=null&&(t.policy=e.policy);const s=await i.bridge.agentInvoke("claude_access_control",{verb:n.verb,payload:t},y);return{content:[{type:"text",text:typeof s=="string"?s:JSON.stringify(s)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}async function O(r,e,i){try{const n=k(r,e);if(!E.has(n.action))throw new Error(`Action not allowed: ${n.action}`);const t=await i.bridge.agentInvoke(n.action,n.params,y);if(t&&Number(t.code??0)!==0)throw new Error(String(t.msg??"invoke failed"));return{content:[{type:"text",text:t?.data!=null?typeof t.data=="string"?t.data:JSON.stringify(t.data):JSON.stringify(t)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}function c(r){return{content:[{type:"text",text:r}]}}export{q as registerClaudeTools};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as l}from"../../core/log/index.js";class c{controlURL="";token="";isConfigured(){return!!(this.controlURL&&this.token)}configure(
|
|
1
|
+
import{log as l}from"../../core/log/index.js";class c{controlURL="";token="";isConfigured(){return!!(this.controlURL&&this.token)}configure(r,e){this.controlURL=r.replace(/\/+$/,""),this.token=e.trim(),l.info("claude-worker-client",`Configured with control URL: ${this.controlURL}`)}async post(r,e,s){if(!this.isConfigured())throw new Error("worker control not configured");const i=new AbortController,o=setTimeout(()=>i.abort(),s);try{const t=await fetch(`${this.controlURL}${r}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.token}`},body:JSON.stringify(e),signal:i.signal}),n=await t.text(),a=n.trim()?JSON.parse(n):{};if(!t.ok)throw new Error(a.error||`worker control failed ${t.status}`);return a}finally{clearTimeout(o)}}isRetryableError(r){const e=r instanceof Error?r.message:String(r);return/fetch failed|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|aborted/i.test(e)}async postWithRetry(r,e,s,i=1){let o;for(let t=0;t<=i;t++)try{return t>0&&l.info("claude-worker-client",`Retrying ${r} attempt=${t+1}`),await this.post(r,e,s)}catch(n){if(o=n,t>=i||!this.isRetryableError(n))break;await new Promise(a=>setTimeout(a,150))}throw o instanceof Error?o:new Error(String(o))}async deliverEvent(r){return this.postWithRetry("/v1/worker/deliver-event",{payload:r},1e4,1)}async deliverStop(r){return this.postWithRetry("/v1/worker/deliver-stop",{payload:r},1e4,1)}async deliverLocalAction(r){return this.postWithRetry("/v1/worker/deliver-local-action",{payload:r},1e4,1)}async ping(){try{return await this.postWithRetry("/v1/worker/ping",{},5e3,1),!0}catch{return!1}}}export{c as ClaudeWorkerClient};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{spawn as x,execSync as y}from"node:child_process";import{randomUUID as v}from"node:crypto";import{mkdir as S}from"node:fs/promises";import{readFileSync as C}from"node:fs";import{join as d}from"node:path";import{homedir as T,tmpdir as I}from"node:os";import{log as o}from"../../core/log/index.js";import{MCP_HTTP_CHANNEL_NAME as
|
|
2
|
-
`),"utf8"),{expectPath:a,pidPath:i}}function h(
|
|
1
|
+
import{spawn as x,execSync as y}from"node:child_process";import{randomUUID as v}from"node:crypto";import{mkdir as S}from"node:fs/promises";import{readFileSync as C}from"node:fs";import{join as d}from"node:path";import{homedir as T,tmpdir as I}from"node:os";import{log as o}from"../../core/log/index.js";import{MCP_HTTP_CHANNEL_NAME as m}from"./protocol-contract.js";function P(t){let e=null,r=0,n=!1,i=!1;const a=v(),s=t.gatewayUrl??"http://127.0.0.1:19580/mcp";return{async start(){await F(t.command,s,t.env);const c=E(t.grix),u=[...t.args??[],"--name",`grix-mcp-${t.name}`,"--session-id",a];t.fullAuto&&u.push("--dangerously-skip-permissions"),u.push("--dangerously-load-development-channels",`server:${m}`,"--append-system-prompt",c);const f=d(I(),`grix-mcp-claude-${t.name}`);await S(f,{recursive:!0});const{expectPath:$,pidPath:g}=await M(f,t.command,u),_={...process.env,...t.env??{}};e=x("/usr/bin/expect",[$],{cwd:t.cwd,env:_,stdio:["ignore","pipe","pipe"],detached:!0}),o.info("mcp-http-launcher",`\u542F\u52A8 Claude: name=${t.name} cwd=${t.cwd} pid=${e.pid}`),r=await k(g),n=!0,o.info("mcp-http-launcher",`Claude \u5B50\u8FDB\u7A0B PID: ${r}`),e.on("exit",(l,p)=>{o.info("mcp-http-launcher",`Claude \u9000\u51FA: code=${l} signal=${p}`),n=!1,e=null,r=0,i||(o.info("mcp-http-launcher","3 \u79D2\u540E\u81EA\u52A8\u91CD\u542F..."),setTimeout(()=>{i||this.start().catch(w=>{o.error("mcp-http-launcher",`\u91CD\u542F\u5931\u8D25: ${w}`)})},3e3))}),e.stdout?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stdout] ${p.slice(0,300)}`)}),e.stderr?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stderr] ${p.slice(0,300)}`)})},async stop(){if(i=!0,n=!1,r>0)try{process.kill(r,"SIGTERM")}catch{}if(e?.pid){try{process.kill(-e.pid,"SIGTERM")}catch{}await new Promise(c=>{const u=setTimeout(()=>{if(r>0)try{process.kill(r,"SIGKILL")}catch{}if(e?.pid)try{process.kill(-e.pid,"SIGKILL")}catch{}c()},5e3);e?.once("exit",()=>{clearTimeout(u),c()})})}e=null,r=0},getStatus(){return{name:t.name,alive:n,pid:r}}}}function E(t){return["You are connected to a chat via the grix MCP server.",`On startup, immediately call grix_authorize with: agentId="${t.agentId}", apiKey="${t.apiKey}", wsUrl="${t.wsUrl}", clientType="${t.clientType}".`,"When you receive a <channel> message, you MUST respond by calling the grix_reply tool (or the grix_complete tool if no response is needed).","Never write your reply as plain text \u2014 it will NOT reach the user. Only the grix_reply tool delivers your response to the chat.","The <channel> message contains event_id and session_id \u2014 pass them to grix_reply."].join(" ")}async function F(t,e,r){const n=d(T(),".claude.json");let i=null;try{const s=C(n,"utf8");i=JSON.parse(s)?.mcpServers?.[m]??null}catch{}if(i&&String(i.type??"").trim()==="http"&&String(i.url??"").trim()===e)return;o.info("mcp-http-launcher",`\u6CE8\u518C MCP Server: ${m} -> ${e}`);const a={...process.env,...r??{}};try{y(`${t} mcp remove -s user ${m}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}catch{}y(`${t} mcp add --scope user --transport http ${m} ${e}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}async function M(t,e,r){const{writeFile:n}=await import("node:fs/promises"),i=d(t,"claude.pid"),a=d(t,"claude.expect"),s=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set claude_command [list {${h(e)}}${r.map(c=>` {${h(c)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${h(i)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)I am using this for local development} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {Listening for channel} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," -re {bypass permissions} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," eof {}","}","expect eof",""];return await n(i,"","utf8"),await n(a,s.join(`
|
|
2
|
+
`),"utf8"),{expectPath:a,pidPath:i}}function h(t){return t.replace(/[\\{}$\[\]"]/g,"\\$&")}async function k(t,e=1e4){const{readFile:r}=await import("node:fs/promises"),n=Math.ceil(e/100);for(let i=0;i<n;i++){try{const a=await r(t,"utf8"),s=parseInt(String(a).trim(),10);if(Number.isFinite(s)&&s>0)return s}catch{}await new Promise(a=>setTimeout(a,100))}return 0}export{P as createMcpHttpLauncher};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
class m{defaultTimeoutMs;onTimeout;timers=new Map;constructor(
|
|
1
|
+
class m{defaultTimeoutMs;onTimeout;timers=new Map;constructor(e){this.defaultTimeoutMs=e.defaultTimeoutMs??9e4,this.onTimeout=e.onTimeout}arm(e,t){this.cancel(e);const s=t?.timeoutMs??this.defaultTimeoutMs,i=Date.now()+s,o=setTimeout(()=>{this.timers.delete(e),this.onTimeout(e).catch(()=>{})},s);return this.timers.set(e,o),i}cancel(e){const t=this.timers.get(e);t&&(clearTimeout(t),this.timers.delete(e))}has(e){return this.timers.has(e)}close(){for(const e of this.timers.values())clearTimeout(e);this.timers.clear()}}export{m as ResultTimeoutManager};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const
|
|
1
|
+
import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const i of c){if(!p&&i.name.startsWith("."))continue;const t=l(a,i.name),e={id:t,name:i.name,is_directory:i.isDirectory()};try{if(i.isDirectory()){const o=await m(t);e.modified_at=o.mtime.toISOString()}else{const o=await m(t);e.size=o.size,e.modified_at=o.mtime.toISOString(),e.mime_type=n(i.name)}}catch{}s.push(e)}return s.sort((i,t)=>i.is_directory!==t.is_directory?i.is_directory?-1:1:i.name.localeCompare(t.name)),s}export{f as listFiles,n as resolveMimeType};
|
package/dist/core/mcp/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const a=[{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"],description:"Query action type."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},keyword:{type:"string",description:"Search keyword."},limit:{type:"integer",description:"Max results."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. Each link is one-time and expires, so call this again to produce a fresh link every time you deliver a file. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds (default 10 minutes)."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Agent introduction (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the text introduction of one of your owner's agents, identified by its numeric agent ID.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id","introduction"]},validation:{required:["agent_id","introduction"],properties:{agent_id:{type:"string"},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:`Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend opens (or reuses) a private session between the owner and that agent, binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session AS THE OWNER so the agent starts working. Because the task is delivered as the owner, write it in the owner's first-person voice and tone \u2014 phrase it the way the owner would speak directly to the agent (e.g. "\u5E2E\u6211\u2026", "\u4F60\u53BB\u2026"), NOT as a third-person relay or as yourself narrating on the owner's behalf.`,inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},cwd:{type:"string",description:"Absolute working directory where the agent should do the work."},task:{type:"string",description:"Text description of the task to perform, written in the owner's first-person voice and tone \u2014 it is delivered into the session as the owner, so phrase it as the owner speaking directly to the agent, not as a third-person relay."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Use ONLY to relay on the owner's behalf into one of the owner's OTHER sessions that you are not part of (e.g. you were dispatched to work and need to drop a note to the owner elsewhere). NEVER use this to send your own reply in a session you are conversing in \u2014 that would make your words show up as the owner's message. To answer in your current conversation, reply normally (or use grix_message_send to send as yourself). The owner must be a member of the target session, and you (the agent) must NOT be a member of it \u2014 sending into a session you belong to is rejected.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}}],d=[{name:"grix_reply",description:"Send a reply message to the specified session. Supports streaming in chunks; the frontend will automatically aggregate them into one complete message. Any content meant for the user \u2014 including your final conclusion at the end of a turn \u2014 MUST be sent through this tool; text written outside this tool is not delivered to the user.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),b=new Set(a.map(e=>e.name)),v=new Set(d.map(e=>e.name)),w=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,I=/[A-Za-z0-9._-]+/;function z(e){return!!(b.has(e)||v.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const k=[...a,...d],x=[...k,...p],P=new Map(x.map(e=>[e.name,e]));function R(e,t){return e==="reply"?{name:"grix_reply",args:_("grix_reply",{event_id:t.event_id,session_id:t.chat_id,text:t.text,quoted_message_id:t.reply_to,is_final:t.final})}:e==="complete"?{name:"grix_complete",args:_("grix_complete",{event_id:t.event_id,status:t.status,msg:t.msg,code:t.code})}:{name:e,args:t}}function l(e){const t=String(e??"").trim();return t?t.match(w)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return m(n);const i=t.match(/(?:chat_id|session_id)\s*=\s*"([A-Za-z0-9._-]+)"/)?.[1];if(i)return i;const o=t.match(/[A-Za-z0-9._-]+/g)??[];for(const s of o)if(!(s==="event_id"||s==="chat_id"||s==="session_id")&&s.length>0)return s;return t.match(I)?.[0]}function m(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function _(e,t){if(e!=="grix_reply"&&e!=="grix_complete")return t;const n={...t},i=l(n.event_id);if(i&&(n.event_id=i),e==="grix_reply"){const o=m(i),r=String(n.session_id??""),s=u(n.session_id),f=/\bevent_id\b|["'<>\s]/.test(r);s&&!(f&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function U(e){return c.has(e)}function G(e,t){switch(e){case"grix_query":return S(t);case"grix_group":return A(t);case"grix_message_send":return T(t);case"grix_message_unsend":return q(t);case"grix_file_link":return M(t);case"grix_admin":return C(t);case"grix_call_owner":return O(t);case"grix_agent_update":return D(t);case"grix_dispatch_agent":return E(t);case"grix_session_send":return N(t);case"grix_chat_state_query":return L(t);case"grix_chat_state_update":return j(t);default:throw new Error(`Unknown tool: ${e}`)}}const g={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search"};function S(e){const t=String(e.action??""),n=g[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.keyword!=null&&(i.keyword=e.keyword),e.limit!=null&&(i.limit=e.limit),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}const y={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function A(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function T(e){const t={session_id:e.sessionId,msg_type:e.msgType??1,content:e.content};return e.quotedMessageId!=null&&(t.quoted_message_id=e.quotedMessageId),e.threadId!=null&&(t.thread_id=e.threadId),{action:"send_msg",params:t}}function q(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function M(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}const h={create_agent:"agent_api_create",list_categories:"agent_category_list",create_category:"agent_category_create",update_category:"agent_category_update",assign_category:"agent_category_assign",rotate_api_key:"agent_api_key_rotate"};function O(e){return{action:"call_owner",params:{session_id:e.session_id}}}function D(e){return{action:"agent_introduction_update",params:{agent_id:e.agent_id,introduction:e.introduction}}}function E(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task}}}function N(e){return{action:"session_send",params:{session_id:e.session_id,content:e.content}}}function L(e){const t={};return e.session_id!=null&&(t.session_id=e.session_id),e.page!=null&&(t.page=e.page),e.page_size!=null&&(t.page_size=e.page_size),e.state!=null&&(t.state=e.state),{action:"chat_state_query",params:t}}function j(e){const t={session_id:e.session_id,state:e.state};return e.reason!=null&&(t.reason=e.reason),{action:"chat_state_update",params:t}}function C(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_admin action: ${t}`);const i={};return e.agentName!=null&&(i.agent_name=e.agentName),e.introduction!=null&&(i.introduction=e.introduction),e.isMain!=null&&(i.is_main=e.isMain),e.agentId!=null&&(i.agent_id=e.agentId),e.categoryId!=null&&(i.category_id=e.categoryId),e.name!=null&&(i.name=e.name),e.parentId!=null&&(i.parent_id=e.parentId),e.sortOrder!=null&&(i.sort_order=e.sortOrder),{action:n,params:i}}const W=new Set([...Object.values(g),...Object.values(y),...Object.values(h),"send_msg","delete_msg","file_link","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update"]),Q={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Q as ACCESS_CONTROL_ACTION_MAP,k as ALL_TOOLS,d as EVENT_TOOLS,x as EXPOSED_TOOLS,W as PHASE1_INVOKE_ACTIONS,b as PHASE1_TOOL_NAMES,v as PHASE2_TOOL_NAMES,a as TOOLS,p as TOOL_ALIASES,P as TOOL_MAP,U as isAlias,z as isGrixInternalToolName,R as mapToolAlias,_ as normalizeEventToolArgs,G as toolCallToInvoke};
|
|
1
|
+
const a=[{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"],description:"Query action type."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},keyword:{type:"string",description:"Search keyword."},limit:{type:"integer",description:"Max results."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. Each link is one-time and expires, so call this again to produce a fresh link every time you deliver a file. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds (default 10 minutes)."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Agent introduction (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the text introduction of one of your owner's agents, identified by its numeric agent ID.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id","introduction"]},validation:{required:["agent_id","introduction"],properties:{agent_id:{type:"string"},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:`Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend opens (or reuses) a private session between the owner and that agent, binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session AS THE OWNER so the agent starts working. Because the task is delivered as the owner, write it in the owner's first-person voice and tone \u2014 phrase it the way the owner would speak directly to the agent (e.g. "\u5E2E\u6211\u2026", "\u4F60\u53BB\u2026"), NOT as a third-person relay or as yourself narrating on the owner's behalf.`,inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},cwd:{type:"string",description:"Absolute working directory where the agent should do the work."},task:{type:"string",description:"Text description of the task to perform, written in the owner's first-person voice and tone \u2014 it is delivered into the session as the owner, so phrase it as the owner speaking directly to the agent, not as a third-person relay."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Use ONLY to relay on the owner's behalf into one of the owner's OTHER sessions that you are not part of (e.g. you were dispatched to work and need to drop a note to the owner elsewhere). NEVER use this to send your own reply in a session you are conversing in \u2014 that would make your words show up as the owner's message. To answer in your current conversation, reply normally (or use grix_message_send to send as yourself). The owner must be a member of the target session, and you (the agent) must NOT be a member of it \u2014 sending into a session you belong to is rejected.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}}],d=[{name:"grix_reply",description:"Send your final reply \u2014 the conclusion the user is waiting for \u2014 to the specified session. This is the message the user treats as your answer; deliver your final result here, and use plain text only for brief progress notes while you work. Supports streaming in chunks; the frontend automatically aggregates them into one complete message. The connector quotes the message being answered automatically, so quoted_message_id is optional (set it only to quote a different earlier message).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),b=new Set(a.map(e=>e.name)),v=new Set(d.map(e=>e.name)),w=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,I=/[A-Za-z0-9._-]+/;function z(e){return!!(b.has(e)||v.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const k=[...a,...d],x=[...k,...p],P=new Map(x.map(e=>[e.name,e]));function R(e,t){return e==="reply"?{name:"grix_reply",args:_("grix_reply",{event_id:t.event_id,session_id:t.chat_id,text:t.text,quoted_message_id:t.reply_to,is_final:t.final})}:e==="complete"?{name:"grix_complete",args:_("grix_complete",{event_id:t.event_id,status:t.status,msg:t.msg,code:t.code})}:{name:e,args:t}}function l(e){const t=String(e??"").trim();return t?t.match(w)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return m(n);const i=t.match(/(?:chat_id|session_id)\s*=\s*"([A-Za-z0-9._-]+)"/)?.[1];if(i)return i;const r=t.match(/[A-Za-z0-9._-]+/g)??[];for(const s of r)if(!(s==="event_id"||s==="chat_id"||s==="session_id")&&s.length>0)return s;return t.match(I)?.[0]}function m(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function _(e,t){if(e!=="grix_reply"&&e!=="grix_complete")return t;const n={...t},i=l(n.event_id);if(i&&(n.event_id=i),e==="grix_reply"){const r=m(i),o=String(n.session_id??""),s=u(n.session_id),f=/\bevent_id\b|["'<>\s]/.test(o);s&&!(f&&r)?n.session_id=s:r&&(n.session_id=r)}return n}function U(e){return c.has(e)}function G(e,t){switch(e){case"grix_query":return S(t);case"grix_group":return A(t);case"grix_message_send":return T(t);case"grix_message_unsend":return q(t);case"grix_file_link":return M(t);case"grix_admin":return C(t);case"grix_call_owner":return O(t);case"grix_agent_update":return D(t);case"grix_dispatch_agent":return E(t);case"grix_session_send":return N(t);case"grix_chat_state_query":return L(t);case"grix_chat_state_update":return j(t);default:throw new Error(`Unknown tool: ${e}`)}}const g={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search"};function S(e){const t=String(e.action??""),n=g[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.keyword!=null&&(i.keyword=e.keyword),e.limit!=null&&(i.limit=e.limit),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}const y={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function A(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function T(e){const t={session_id:e.sessionId,msg_type:e.msgType??1,content:e.content};return e.quotedMessageId!=null&&(t.quoted_message_id=e.quotedMessageId),e.threadId!=null&&(t.thread_id=e.threadId),{action:"send_msg",params:t}}function q(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function M(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}const h={create_agent:"agent_api_create",list_categories:"agent_category_list",create_category:"agent_category_create",update_category:"agent_category_update",assign_category:"agent_category_assign",rotate_api_key:"agent_api_key_rotate"};function O(e){return{action:"call_owner",params:{session_id:e.session_id}}}function D(e){return{action:"agent_introduction_update",params:{agent_id:e.agent_id,introduction:e.introduction}}}function E(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task}}}function N(e){return{action:"session_send",params:{session_id:e.session_id,content:e.content}}}function L(e){const t={};return e.session_id!=null&&(t.session_id=e.session_id),e.page!=null&&(t.page=e.page),e.page_size!=null&&(t.page_size=e.page_size),e.state!=null&&(t.state=e.state),{action:"chat_state_query",params:t}}function j(e){const t={session_id:e.session_id,state:e.state};return e.reason!=null&&(t.reason=e.reason),{action:"chat_state_update",params:t}}function C(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_admin action: ${t}`);const i={};return e.agentName!=null&&(i.agent_name=e.agentName),e.introduction!=null&&(i.introduction=e.introduction),e.isMain!=null&&(i.is_main=e.isMain),e.agentId!=null&&(i.agent_id=e.agentId),e.categoryId!=null&&(i.category_id=e.categoryId),e.name!=null&&(i.name=e.name),e.parentId!=null&&(i.parent_id=e.parentId),e.sortOrder!=null&&(i.sort_order=e.sortOrder),{action:n,params:i}}const W=new Set([...Object.values(g),...Object.values(y),...Object.values(h),"send_msg","delete_msg","file_link","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update"]),Q={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Q as ACCESS_CONTROL_ACTION_MAP,k as ALL_TOOLS,d as EVENT_TOOLS,x as EXPOSED_TOOLS,W as PHASE1_INVOKE_ACTIONS,b as PHASE1_TOOL_NAMES,v as PHASE2_TOOL_NAMES,a as TOOLS,p as TOOL_ALIASES,P as TOOL_MAP,U as isAlias,z as isGrixInternalToolName,R as mapToolAlias,_ as normalizeEventToolArgs,G as toolCallToInvoke};
|
package/dist/log.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as
|
|
2
|
-
`)},error(o,r,...
|
|
1
|
+
import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as i}from"node:path";import{homedir as m}from"node:os";const n=i(m(),".grix"),s={base:n,config:i(n,"config"),log:i(n,"log"),data:i(n,"data")};function S(){for(const o of Object.values(s))f(o)||l(o,{recursive:!0})}let a=null;function $(){const o=new Date().toISOString().slice(0,10),r=i(s.log,`grix-acp-${o}.log`);a=g(r,{flags:"a"})}function c(){return new Date().toISOString().slice(11,19)}const u={info(o,r,...t){const e=`${c()} [${o}] ${r}${t.length?" "+t.map(String).join(" "):""}`;console.log(e),a?.write(e+`
|
|
2
|
+
`)},error(o,r,...t){const e=`${c()} [${o}] ERROR ${r}${t.length?" "+t.map(String).join(" "):""}`;console.error(e),a?.write(e+`
|
|
3
3
|
`)}};export{s as GRIX_PATHS,S as ensureGrixDirs,$ as initLogger,u as log};
|
package/dist/mcp/stdio/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import R from"node:http";import S from"node:net";import c from"node:process";import{Server as v}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as A}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as I,ListToolsRequestSchema as x}from"@modelcontextprotocol/sdk/types.js";import{toolCallToInvoke as P,mapToolAlias as
|
|
3
|
-
`),c.exit(1));const
|
|
4
|
-
`),await new Promise(
|
|
5
|
-
`)}}async function U(){for(;_&&d.length>0;){const
|
|
6
|
-
`);r=
|
|
7
|
-
`)}),
|
|
8
|
-
`)})}async function
|
|
9
|
-
`),await U()}
|
|
2
|
+
import R from"node:http";import S from"node:net";import c from"node:process";import{Server as v}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as A}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as I,ListToolsRequestSchema as x}from"@modelcontextprotocol/sdk/types.js";import{toolCallToInvoke as P,mapToolAlias as k,EXPOSED_TOOLS as L,TOOL_MAP as $,PHASE2_TOOL_NAMES as C}from"../../core/mcp/tools.js";function T(e,t){const n=c.argv.slice(2);for(let r=0;r<n.length;r++)if(n[r]===`--${e}`&&n[r+1])return n[r+1];return c.env[t]||void 0}const E=T("handle-url","GRIX_CONNECTOR_INTERNAL_API")??"";E||(c.stderr.write(`FATAL: --handle-url <url> required (or set GRIX_CONNECTOR_INTERNAL_API)
|
|
3
|
+
`),c.exit(1));const b=parseInt(T("notify-port","GRIX_MCP_NOTIFY_PORT")??"0",10),u=[1e3,2e3,4e3];function q(e){if(e instanceof Error){const t=e.message;if(t.includes("ECONNREFUSED")||t.includes("ECONNRESET")||t.includes("ETIMEDOUT")||t.includes("socket hang up"))return!0;if(t.includes("invoke timeout"))return!1}return!1}async function y(e,t,n=15e3,r=0){try{return await D(e,t,n)}catch(o){if(r>0&&q(o)){const s=u[u.length-r]??u[u.length-1];return c.stderr.write(`[grix-stdio] invokeApi retry (${u.length-r+1}/${u.length}) action=${e} after ${s}ms: ${o instanceof Error?o.message:o}
|
|
4
|
+
`),await new Promise(i=>setTimeout(i,s)),y(e,t,n,r-1)}throw o}}function D(e,t,n){const r=new URL(E),o=new URL("/api/invoke",r),s=r.searchParams.get("token")??"";s&&o.searchParams.set("token",s);const i=JSON.stringify({action:e,params:t,timeout_ms:n});return new Promise((l,a)=>{const w=setTimeout(()=>a(new Error("invoke timeout")),n+5e3),p=R.request(o,{method:"POST",headers:{"content-type":"application/json","content-length":Buffer.byteLength(i),...s?{authorization:`Bearer ${s}`}:{}}},h=>{const N=[];h.on("data",m=>N.push(m)),h.on("end",()=>{clearTimeout(w);const m=Buffer.concat(N).toString("utf8");try{const g=JSON.parse(m);g.ok?l(g.data??null):a(new Error(g.error??"invoke failed"))}catch{a(new Error(`invalid response: ${m.slice(0,200)}`))}})});p.on("error",h=>{clearTimeout(w),a(h)}),p.write(i),p.end()})}const f=new v({name:"grix",version:"1.0.0"},{capabilities:{experimental:{"claude/channel":{},"claude/channel/permission":{}},tools:{}},instructions:['Messages arrive as <channel source="grix-claude" chat_id="..." event_id="..." message_id="..." user_id="...">text</channel>.','Both your plain text and the reply tool are delivered to the user. You are talking directly to the user, so address them in the second person ("you"); do not narrate in the third person about them or describe what you "already replied".',"Use plain text for short progress notes while you work; deliver your final conclusion through the reply tool \u2014 that is the message the user treats as your answer. Put the conclusion only in the reply tool, not also as plain text. The connector automatically quotes the message you are answering, so you normally do not need reply_to (set it only to quote a different earlier message).","If you intentionally do not want to send any visible message, call the complete tool with event_id and a final status."].join(" ")});f.setRequestHandler(x,async()=>({tools:L.map(e=>({name:e.name,description:e.description,inputSchema:e.inputSchema}))})),f.setRequestHandler(I,async e=>{const t=String(e.params.name??""),n=e.params.arguments??{},r=k(t,n),o=r.name,s=r.args;if(!$.has(t))return{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0};try{if(C.has(o)){const a=await y("event_tool_call",{tool_name:o,arguments:s},3e4,0);return{content:[{type:"text",text:typeof a=="string"?a:JSON.stringify(a,null,2)}]}}const i=P(o,s),l=await y(i.action,i.params);return{content:[{type:"text",text:JSON.stringify(l,null,2)}]}}catch(i){const l=i instanceof Error?i.message:String(i);return{content:[{type:"text",text:JSON.stringify({error:l})}],isError:!0}}});let _=!1;const d=[],F=100;async function O(e,t){if(!_){d.push({method:e,params:t}),d.length>F&&d.shift();return}try{await f.notification({method:e,params:t})}catch(n){c.stderr.write(`DISPATCH_ERROR:${e}:${n}
|
|
5
|
+
`)}}async function U(){for(;_&&d.length>0;){const e=d.shift();await O(e.method,e.params)}}function Y(e){if(e<=0)return;const t=S.createServer(n=>{let r="";n.setEncoding("utf8"),n.on("data",o=>{r+=o;const s=r.split(`
|
|
6
|
+
`);r=s.pop()??"";for(const i of s){const l=i.trim();if(l)try{const a=JSON.parse(l);a.method&&O(a.method,a.params??{}).catch(()=>{})}catch{}}}),n.on("error",()=>{})});t.listen(e,"127.0.0.1",()=>{c.stderr.write(`NOTIFY_READY:${e}
|
|
7
|
+
`)}),t.on("error",n=>{c.stderr.write(`NOTIFY_ERROR: ${n.message}
|
|
8
|
+
`)})}async function M(){Y(b);const e=new A;await f.connect(e),_=!0,c.stderr.write(`[MAIN] MCP connected, ready to dispatch notifications
|
|
9
|
+
`),await U()}M().catch(e=>{c.stderr.write(`FATAL: ${e}
|
|
10
10
|
`),c.exit(1)});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as n from"node:net";const i={bind:"127.0.0.1",port:0,endpoint:"/mcp",sessionTimeoutMs:18e5,invokeTimeoutMs:3e4};function s(u){const e={bind:u?.bind??i.bind,port:u?.port??i.port,endpoint:u?.endpoint??i.endpoint,sessionTimeoutMs:u?.sessionTimeoutMs??i.sessionTimeoutMs,invokeTimeoutMs:u?.invokeTimeoutMs??i.invokeTimeoutMs,allowedOrigins:u?.allowedOrigins,allowedHosts:u?.allowedHosts};return t(e.bind),e.port!==0&&o(e.port),r(e.sessionTimeoutMs),e}function t(u){if(!u||!n.isIPv4(u)&&!n.isIPv6(u))throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: bind \u5730\u5740 "${u}" \u4E0D\u662F\u5408\u6CD5\u7684 IPv4 \u6216 IPv6 \u5730\u5740`)}function o(u){if(!Number.isInteger(u)||u<1||u>65535)throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: port \u503C ${u} \u4E0D\u5728\u5408\u6CD5\u8303\u56F4 1-65535 \u5185\u6216\u4E0D\u662F\u6574\u6570`)}function r(u){if(!Number.isInteger(u)||u<1e3||u>864e5)throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: session_timeout_ms \u503C ${u} \u4E0D\u5728\u5408\u6CD5\u8303\u56F4 1000-86400000 \u5185`)}export{s as createDefaultGatewayConfig};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const a=3e4;class c{connectionManager;onDisconnected;bindings=new Map;constructor(n,i){this.connectionManager=n,this.onDisconnected=i}async bind(n,i){if(this.bindings.has(n))throw new Error(`Session ${n} is already bound to a connection`);const e=await this.connectWithTimeout(i),t=[],s=e.onDisconnected(()=>{this.removeBinding(n),this.onDisconnected(n)});return t.push(s),this.bindings.set(n,{sessionId:n,handle:e,subscriptions:t}),e}getHandle(n){return this.bindings.get(n)?.handle}unbind(n){const i=this.bindings.get(n);if(i){this.bindings.delete(n);for(const e of i.subscriptions)e();i.handle.disconnect()}}unbindAll(){const n=[...this.bindings.keys()];for(const i of n)this.unbind(i)}connectWithTimeout(n){return new Promise((i,e)=>{let t=!1;const s=setTimeout(()=>{t||(t=!0,e(new Error("Connection bind timeout after 30000ms")))},3e4);this.connectionManager.connect({agentId:n.agentId,apiKey:n.apiKey,url:n.wsUrl,clientType:n.clientType,capabilities:["agent_invoke"],adapterHint:`${n.clientType}/base`},{maxRetries:0}).then(o=>{t?o.disconnect():(t=!0,clearTimeout(s),i(o))}).catch(o=>{t||(t=!0,clearTimeout(s),e(o))})})}removeBinding(n){const i=this.bindings.get(n);if(i){this.bindings.delete(n);for(const e of i.subscriptions)e()}}}export{c as ConnectionBindingImpl};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function a(
|
|
1
|
+
function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toolCallToInvoke as i}from"../../core/mcp/tools.js";import{ToolRegistryImpl as l}from"./tool-registry.js";import{validateToolArgs as a}from"./tool-schemas.js";import{isEventTool as p,executeEventTool as d}from"./event-tool-executor.js";class y{registry;constructor(){this.registry=new l}async execute(
|
|
1
|
+
import{toolCallToInvoke as i}from"../../core/mcp/tools.js";import{ToolRegistryImpl as l}from"./tool-registry.js";import{validateToolArgs as a}from"./tool-schemas.js";import{isEventTool as p,executeEventTool as d}from"./event-tool-executor.js";class y{registry;constructor(){this.registry=new l}async execute(r,e,t,n){if(!this.registry.hasTool(e))return this.errorResult(`\u672A\u77E5\u5DE5\u5177: ${e}`);const s=a(e,t);if(!s.valid)return this.errorResult(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${s.error}`);if(r.status!=="ready")return this.errorResult(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(p(e))return this.executeEventTool(r,e,t);const o=i(e,t);try{const u=await r.agentInvoke(o.action,o.params,n);return this.normalizeResult(u)}catch(u){const c=u instanceof Error?u.message:String(u);return c.toLowerCase().includes("timeout")?this.errorResult(`\u8C03\u7528\u8D85\u65F6: ${c}`):this.errorResult(`\u8C03\u7528\u5931\u8D25: ${c}`)}}normalizeResult(r){if(r==null||typeof r!="object")return this.successResult(r??null);const e=r,t=typeof e.code=="number"?e.code:0;if(t===0){const s="data"in e?e.data:null;return this.successResult(s??null)}const n=typeof e.msg=="string"?e.msg:"\u672A\u77E5\u9519\u8BEF";return this.errorResult(`\u4E0A\u6E38\u9519\u8BEF [code=${t}]: ${n}`)}successResult(r){return{content:[{type:"text",text:JSON.stringify(r)}],isError:!1}}errorResult(r){return{content:[{type:"text",text:r}],isError:!0}}async executeEventTool(r,e,t){return e==="grix_access_control"?this.executeAccessControl(r,t):d(r,e,t)}async executeAccessControl(r,e){const t=String(e.action??""),n={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"}[t];if(!n)return this.errorResult(`\u672A\u77E5 access_control action: ${t}`);const s={};e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy);try{const o=await r.agentInvoke("claude_access_control",{verb:n,payload:s},3e4);return this.successResult(o)}catch(o){const u=o instanceof Error?o.message:String(o);return this.errorResult(`access_control \u8C03\u7528\u5931\u8D25: ${u}`)}}}export{y as ToolExecutorImpl};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TOOLS as
|
|
1
|
+
import{TOOLS as o,EVENT_TOOLS as s}from"../../core/mcp/tools.js";const e=new Set(["grix_query","grix_group","grix_message_send","grix_message_unsend","grix_admin"]),r=new Set(["grix_reply","grix_complete","grix_event_ack","grix_composing","grix_access_control","grix_status"]);class a{tools;toolMap;constructor(){this.tools=[...o.filter(t=>e.has(t.name)),...s.filter(t=>r.has(t.name))],this.toolMap=new Map(this.tools.map(t=>[t.name,t]))}getTools(){return this.tools}getTool(t){return this.toolMap.get(t)}hasTool(t){return this.toolMap.has(t)}}export{a as ToolRegistryImpl};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const o={required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}},a={required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}},p={required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}},m={required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}},g={required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}},y={required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}},d={required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}},c={required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}},l={required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}},_={required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}},f={required:[],properties:{}},C={grix_query:o,grix_group:a,grix_message_send:p,grix_message_unsend:m,grix_admin:g,grix_reply:y,grix_complete:d,grix_event_ack:c,grix_composing:l,grix_access_control:_,grix_status:f};function B(
|
|
1
|
+
const o={required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}},a={required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}},p={required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}},m={required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}},g={required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}},y={required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}},d={required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}},c={required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}},l={required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}},_={required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}},f={required:[],properties:{}},C={grix_query:o,grix_group:a,grix_message_send:p,grix_message_unsend:m,grix_admin:g,grix_reply:y,grix_complete:d,grix_event_ack:c,grix_composing:l,grix_access_control:_,grix_status:f};function B(r,t){const e=C[r];if(!e)return{valid:!1,error:`\u672A\u77E5\u5DE5\u5177: ${r}`};for(const i of e.required)if(t[i]===void 0||t[i]===null)return{valid:!1,error:`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${i}`};for(const[i,u]of Object.entries(t)){if(u==null)continue;const n=e.properties[i];if(!n)continue;const s=$(i,u,n);if(s)return{valid:!1,error:s}}return{valid:!0}}function $(r,t,e){switch(e.type){case"string":if(typeof t!="string")return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B string\uFF0C\u5B9E\u9645 ${typeof t}`;if(e.maxLength!==void 0&&t.length>e.maxLength)return`\u53C2\u6570 ${r} \u8D85\u8FC7\u6700\u5927\u957F\u5EA6 ${e.maxLength}\uFF0C\u5B9E\u9645 ${t.length}`;if(e.enum&&!e.enum.includes(t))return`\u53C2\u6570 ${r} \u503C "${t}" \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.enum.join(", ")}]`;break;case"integer":if(typeof t!="number"||!Number.isInteger(t))return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B integer\uFF0C\u5B9E\u9645 ${typeof t=="number"?"\u6D6E\u70B9\u6570":typeof t}`;if(e.minimum!==void 0&&t<e.minimum)return`\u53C2\u6570 ${r} \u503C ${t} \u5C0F\u4E8E\u6700\u5C0F\u503C ${e.minimum}`;if(e.maximum!==void 0&&t>e.maximum)return`\u53C2\u6570 ${r} \u503C ${t} \u5927\u4E8E\u6700\u5927\u503C ${e.maximum}`;if(e.enum&&!e.enum.includes(t))return`\u53C2\u6570 ${r} \u503C ${t} \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.enum.join(", ")}]`;break;case"boolean":if(typeof t!="boolean")return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B boolean\uFF0C\u5B9E\u9645 ${typeof t}`;break;case"array":if(!Array.isArray(t))return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B array\uFF0C\u5B9E\u9645 ${typeof t}`;if(e.maxItems!==void 0&&t.length>e.maxItems)return`\u53C2\u6570 ${r} \u8D85\u8FC7\u6700\u5927\u5143\u7D20\u6570 ${e.maxItems}\uFF0C\u5B9E\u9645 ${t.length}`;if(e.items)for(let i=0;i<t.length;i++){const u=t[i];if(e.items.type==="string"&&typeof u!="string")return`\u53C2\u6570 ${r}[${i}] \u7C7B\u578B\u9519\u8BEF: \u671F\u671B string\uFF0C\u5B9E\u9645 ${typeof u}`;if(e.items.type==="integer"){if(typeof u!="number"||!Number.isInteger(u))return`\u53C2\u6570 ${r}[${i}] \u7C7B\u578B\u9519\u8BEF: \u671F\u671B integer\uFF0C\u5B9E\u9645 ${typeof u}`;if(e.items.enum&&!e.items.enum.includes(u))return`\u53C2\u6570 ${r}[${i}] \u503C ${u} \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.items.enum.join(", ")}]`}}break}}export{B as validateToolArgs};
|
package/openclaw-plugin/index.js
CHANGED
|
@@ -9174,7 +9174,7 @@ var TOOLS = [
|
|
|
9174
9174
|
var EVENT_TOOLS = [
|
|
9175
9175
|
{
|
|
9176
9176
|
name: "grix_reply",
|
|
9177
|
-
description: "Send
|
|
9177
|
+
description: "Send your final reply \u2014 the conclusion the user is waiting for \u2014 to the specified session. This is the message the user treats as your answer; deliver your final result here, and use plain text only for brief progress notes while you work. Supports streaming in chunks; the frontend automatically aggregates them into one complete message. The connector quotes the message being answered automatically, so quoted_message_id is optional (set it only to quote a different earlier message).",
|
|
9178
9178
|
inputSchema: {
|
|
9179
9179
|
type: "object",
|
|
9180
9180
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grix-connector",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|