grix-connector 4.9.0 → 4.9.1
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/README.md +2 -0
- package/dist/adapter/claude/claude-adapter.js +20 -20
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.9.1.tgz +0 -0
- package/dist/assets/dsh-bridge/manifest.json +6 -6
- package/dist/core/mcp/internal-api-server.js +1 -1
- package/dist/core/mcp/tools.js +1 -1
- package/dist/default-skills/grix-agent-dispatch/SKILL.md +1 -1
- package/dist/mcp/stdio/server.js +9 -8
- package/dist/mcp/stream-http/security.js +1 -1
- package/openclaw-plugin/index.js +4 -2
- package/openclaw-plugin/skills/grix-agent-dispatch/SKILL.md +1 -1
- package/package.json +1 -1
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.9.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ The ACP adapter (Agent Client Protocol over JSON-RPC) backs Gemini, Qwen, Copilo
|
|
|
53
53
|
|
|
54
54
|
The generic entry ships the shared ACP surface (session binding, permission prompts, model/mode switching, file browsing). Per-agent extras — an installation guide, a dedicated toolbar, session usage parsing — still require a registered `client_type`.
|
|
55
55
|
|
|
56
|
+
Two limits of the generic entry are deliberate and will not be changed. Skill discovery for `client_type: "acp"` falls back to the Gemini layout, so it reads `~/.gemini/skills` and shares that directory with a real Gemini agent on the same machine. `get_session_usage` is not declared for it, because there is no parser for an unknown CLI's usage output. A CLI that needs its own skill directory or usage parsing must be registered as its own `client_type`.
|
|
57
|
+
|
|
56
58
|
For DeepSeek Harness, install the official CLI and complete its login/profile setup before starting the connector:
|
|
57
59
|
|
|
58
60
|
```bash
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import{spawn as te,spawnSync as ie,execSync as se,execFile as et}from"node:child_process";import{promisify as tt}from"node:util";import{randomUUID as J}from"node:crypto";import{killProcessGroup as H,hasChildProcesses as it,resolveCommandPath as st,resolveWindowsExecInvocation as nt}from"../../core/runtime/spawn.js";import{chmodSync as ne,readdirSync as re,readFileSync as j,realpathSync as Ee,rmSync as rt,statSync as b,truncateSync as at,existsSync as C,openSync as ot,writeSync as lt,closeSync as ct,renameSync as ye,mkdirSync as _e,constants as Se,watch as dt}from"node:fs";import{appendFile as ut,mkdir as M,readFile as A,rm as we,stat as Pe,writeFile as O}from"node:fs/promises";import{dirname as ht,isAbsolute as ft,join as f,resolve as pt}from"node:path";import{homedir as I,tmpdir as mt}from"node:os";import ae from"node:net";import{EventEmitter as Ce}from"node:events";import{fileURLToPath as vt}from"node:url";let W=null;if(process.platform==="win32")try{W=await import("node-pty")}catch{W=null}import{resolveRuntimePaths as _}from"../../core/config/index.js";import{normalizeClaudeEffort as gt,normalizeClaudeEnvEffort as Et}from"../../core/config/claude-direct-env.js";import{SESSION_MODE_IDS as q}from"./protocol-contract.js";import{buildSessionContextBlock as yt}from"../shared/session-context.js";import{log as o}from"../../core/log/index.js";import{ActivityStatusManager as _t}from"./activity-status-manager.js";import{HookSignalStore as St}from"../../core/hooks/hook-signal-store.js";import{QuestionStore as Te}from"../../core/persistence/question-store.js";import{PermissionStore as G}from"../../core/persistence/permission-store.js";import{InternalApiServer as wt}from"../../core/mcp/internal-api-server.js";import{executeEventTool as Pt,isEventTool as Ct}from"../../core/mcp/event-tool-executor.js";import{validateToolArgs as Tt}from"../../core/mcp/tool-schemas.js";import{isGrixInternalToolName as At,normalizeEventToolArgs as kt}from"../../core/mcp/tools.js";import{scanSkills as bt}from"./skill-scanner.js";import{syncDefaultSkillsToDir as It}from"../../default-skills/index.js";import{clearSessionJsonlDriftCache as Rt,countPendingBackgroundTasks as $t,noteSessionJsonlPath as Ot,extractLastAssistantEntry as z,extractTextBlocksAfterOffset as Dt,hasTerminalToolResultAfterOffset as Ae,probeSessionTurnState as oe,resolveSessionJsonlPath as B}from"./usage-parser.js";import{readSettingsEnv as Lt}from"./model-list.js";import{resolveCliPath as xt,getCliVersion as Mt}from"../../core/util/cli-probe.js";import{GRIX_RELAY_ANTHROPIC_API_KEY as ke}from"../../core/proxy/index.js";const F="grix",Q={developmentChannels:!0,maxTurns:!0};function Ft(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 be(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,a=`Question ${s+1}`,c=String(r.header??r.question??a).trim()||a,l=String(r.prompt??r.question??"").trim();if(!l)continue;const u=Ft(r.options),h=r.multiSelect===!0||r.multi_select===!0;i.push({header:c,prompt:l,...u?{options:u}:{},...h?{multi_select:!0}:{}})}return i.length>0?i:null}function Nt(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 jt=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],Bt=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],Ut=/API Error:\s*400.*(server_tool_use\.id|messages\.\d+\.content)/,Ie=300*1e3,Re=300*1e3,$e=Re+30*1e3,Oe=3e3,Ht=50,qt=1800*1e3,Gt=["CLAUDE_CODE_ENABLE_TELEMETRY","CLAUDE_CODE_ENHANCED_TELEMETRY_BETA","OTEL_LOGS_EXPORTER","OTEL_TRACES_EXPORTER","OTEL_METRICS_EXPORTER","OTEL_EXPORTER_OTLP_PROTOCOL","OTEL_EXPORTER_OTLP_ENDPOINT","OTEL_EXPORTER_OTLP_HEADERS","OTEL_LOGS_EXPORT_INTERVAL","OTEL_TRACES_EXPORT_INTERVAL","OTEL_LOG_USER_PROMPTS","OTEL_LOG_ASSISTANT_RESPONSES","OTEL_LOG_TOOL_DETAILS","OTEL_LOG_TOOL_CONTENT","OTEL_LOG_RAW_API_BODIES"];function Y(d=process.env){const e={...d},t=new Set(Gt.map(i=>i.toLowerCase()));for(const i of Object.keys(e))t.has(i.toLowerCase())&&delete e[i];return e}const De=60*1e3,Le=600*1e3,zt=250;function xe(d,e){const t=Number(process.env[d]);return Number.isFinite(t)&&t>0?t:e}function Me(d){return d.trim().replace(/[^a-zA-Z0-9._-]+/gu,"_")||"unknown"}function Jt(d){const e=st("expect",d);return ft(e)?e:null}function V(d){try{return C(d)?b(d).size:0}catch{return 0}}function Fe(d){const e=[],t=i=>{let s;try{s=re(i,{withFileTypes:!0})}catch{return}for(const n of s){const r=f(i,n.name);n.isDirectory()?t(r):n.isFile()&&e.push(r)}};return t(d),e.sort((i,s)=>Ne(i)-Ne(s)||i.localeCompare(s))}function Ne(d){try{return b(d).mtimeMs}catch{return 0}}const je=xe("GRIX_RESCUE_SETTLE_POLL_MS",2e3),Be=xe("GRIX_RESCUE_SETTLE_BUDGET_MS",12e4),Wt=300*1e3,Qt="The model's tool call could not be parsed (retry also failed).",Yt="[\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",Vt="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.",Kt=2*1e3,le=90*1e3,Ue=1800*1e3,He=15*1e3,Xt=60*1e3,qe=10*1e3,Zt=30*1e3,ei=30*1e3;function ti(d){const e=d.tool_name||"";if(e==="mcp__grix__grix_dispatch_agent")return"\u540E\u53F0\u534F\u8C03\u5B50 agent";if(e.startsWith("mcp__grix__"))return"\u540E\u53F0\u8C03\u7528 Grix \u5DE5\u5177";if(e.startsWith("mcp__"))return"\u540E\u53F0\u8C03\u7528\u5DE5\u5177";switch(e){case"Bash":return"\u540E\u53F0\u6267\u884C\u547D\u4EE4";case"Read":case"Edit":case"Write":case"NotebookEdit":return"\u540E\u53F0\u8BFB\u5199\u6587\u4EF6";case"Task":return"\u540E\u53F0\u8FD0\u884C\u5B50\u4EFB\u52A1";default:return e?`\u540E\u53F0\u8FD0\u884C\uFF1A${e}`:"\u540E\u53F0\u4EFB\u52A1\u8FDB\u884C\u4E2D"}}function ii(d){try{return process.kill(d,0),!0}catch(e){return e.code==="EPERM"}}const si=12e4,ni=3e4,ri=6e4,ai=3e3,K=1e3,Ge=45e3,oi=18e3,ce=8192,li=2e3,ci=2e3,di=/(do you want to proceed\?[\s\S]*?\d\.\s*yes)|(only you can approve)/i,ui=/(do you want to proceed)|(only you can approve)|(\d\.\s*yes)|(\d\.\s*no)|(esc to cancel)|(tab to amend)/i,hi=/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i,de="__grix_key__ ";let ze=!1;const ue=new Set;async function fi(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=ae.createServer();r.once("error",n),r.listen(0,"127.0.0.1",()=>{const a=r.address(),c=typeof a=="object"&&a?a.port:0;r.close(l=>{if(l){n(l);return}s(c)})})});if(i>0&&!e.has(i)&&!ue.has(i))return ue.add(i),i}throw new Error("\u65E0\u6CD5\u5206\u914D MCP \u901A\u77E5\u7AEF\u53E3")}function pi(d){d>0&&ue.delete(d)}const mi=["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.","Local transcript availability, session binding, recovery mode, and connector history-loss diagnostics are internal operational details. Do not mention them, ask the user to choose a recovery mode, or say that history/context was lost unless the user explicitly asks to debug that mechanism.",'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 and the one the connector quotes. Your plain text reaches the user too \u2014 it is not private scratch space, so it is a real channel for progress and for anything the user needs mid-turn; you just need not restate the full conclusion as plain text once it is in the reply. 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).","Make the reply tool call the last thing you do in a turn and put your whole conclusion inside it: once you have called reply, any further text you write in the same turn is dropped, not delivered. Progress notes go before the reply; the reply ends the turn.",'Never explain the delivery mechanics to the user or tell them you "already sent it via the reply tool" \u2014 that is internal plumbing they neither see nor care about. If the user says they did not receive or cannot read something, do not insist you already sent it; assume the delivery or the format failed and send it again, switching format when that is the likely cause (e.g. render a diagram to an image, strip code fences, upload a file).',"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)."];function vi(d,e,t){const i=[...mi];e&&i.unshift(yt(e,t));try{const s=d?.agentName?.trim().slice(0,200),n=d?.introduction?.trim().slice(0,2e3);if(s||n){const r=[s?`Your name is "${s}".`:"",n?`Your self-introduction: ${n}`:""].filter(Boolean).join(" ");i.unshift(r)}}catch{}return i.join(" ")}function Je(d){return String(d??"").trim().toLowerCase()===q.approval?q.approval:q.fullAuto}class os extends Ce{type="claude";config;bridgeCallbacks;mcpServerProcess=null;internalApi=null;claudeProcess=null;claudePty=null;singleTurnFallbackProcesses=new WeakSet;singleTurnFallbackPtys=new WeakSet;singleTurnFallbackChild=null;spawnPromise=null;lifecycleVersion=0;sessionId="";alive=!1;stopped=!1;activeEvent=null;completedAuditBoundaries=new Map;activeEventIdleTimer=null;activeEventHardTimer=null;activeEventPostReplyTimer=null;activeEventPostReplyWatcher=null;activeEventPostReplyPoll=null;rescueSettleTimer=null;stopHookBarrierSessionId=null;stopHookBarrierTimer=null;selfDrivenActive=!1;selfDrivenLastSignalAt=0;selfDrivenSweepTimer=null;selfDrivenReported=!1;selfDrivenReportTimer=null;selfDrivenLabel=null;compacting=!1;compactingTimer=null;compactionDoneResolver=null;mcpServerReady=!1;mcpStartupFailureHandled=!1;mcpChannelBroken=!1;channelGateClosed=!1;startupChannelListening=!1;startupChannelListeningAt=0;startupDialogSeenAt=0;pendingMcpFailureTimer=null;sessionIdConflictDetected=!1;sessionIdConflictRetriedEventIds=new Set;malformedToolRetriedEventIds=new Set;runtimeResolver=null;activityManager=null;deferredModelId=null;claudeCliCapabilities=null;claudeChildPid=0;claudeCliSessionId="";claudeSessionCwd="";claudeMcpConfigPath="";expectRunDir="";auditEvidencePaths=null;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 wt,this.internalApi.setInvokeHandler(async(e,t,i,s)=>this.handleInternalInvoke(e,t,i,s)),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,o.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(o.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.claudeCliSessionId&&Rt(this.claudeCliSessionId),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.startupDialogSeenAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null),this.terminalPermissionKeyTimer&&(clearTimeout(this.terminalPermissionKeyTimer),this.terminalPermissionKeyTimer=null),this.terminalPermissionPromptTail="",this.terminalPermissionPromptVisible=!1,this.pendingPermissions.size>0){const s=_(),n=new G(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();we(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&&(H(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{}H(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.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 gi(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&&!ii(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,backgroundBusy:this.selfDrivenReported,sessions:0,details:this.sessionState?{sessionState:this.sessionState}:void 0}}getActiveEventIds(){return this.activeEvent?[this.activeEvent.eventId]:[]}takeAuditBoundary(e){const t=this.completedAuditBoundaries.get(e);return t&&this.completedAuditBoundaries.delete(e),t}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 it(e,t)}async probe(e){const t=this.config.command||"claude",i=await xt(t),s=i!==null;let n=null,r;if(s){const h=await Mt(t);n=h.version,h.error&&(r=h.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const c=(Lt().ANTHROPIC_BASE_URL??process.env.ANTHROPIC_BASE_URL??"").trim()||null,l=(this.sessionState?.model?.id??(process.env.ANTHROPIC_MODEL??"").trim())||null;let u={attempted:!1,ok:!1,latency_ms:null};if(e?.conversation&&s){const h=Date.now();try{const p=tt(et),m=["-p","ping","--output-format","json"];this.getClaudeCliCapabilities(t).maxTurns&&m.push("--max-turns","1");const v=nt(t,m);await p(v.command,v.args,{timeout:e.timeoutMs??8e3,encoding:"utf-8",windowsVerbatimArguments:v.windowsVerbatimArguments,env:Y()}),u={attempted:!0,ok:!0,latency_ms:Date.now()-h}}catch(p){const m=p,v=m.killed?"conversation_timeout":"conversation_failed";u={attempted:!0,ok:!1,latency_ms:Date.now()-h,error:{code:v,message:m.message??String(p)}}}}return{cli:{command:t,installed:s,path:i,version:n,...r?{error:r}:{}},conversation:u,config:{model:l,base_url:c,source:{model:this.sessionState?.model?.id?"runtime":process.env.ANTHROPIC_MODEL?"env":"unknown",base_url:c?"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=B(e,this.claudeSessionCwd);try{const i=b(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=bt({mode:"claude",projectDir:this.claudeSessionCwd}),r=n.map(a=>{const c=a.trigger?` (${a.trigger})`:"",l=a.pluginName?` [plugin:${a.pluginName}]`:` [${a.source}]`;return`- ${a.name}${c}${l}: ${a.description}`});return{status:"ok",message:r.length>0?r.join(`
|
|
1
|
+
import{spawn as te,spawnSync as ie,execSync as se,execFile as tt}from"node:child_process";import{promisify as it}from"node:util";import{randomUUID as J}from"node:crypto";import{killProcessGroup as H,hasChildProcesses as st,resolveCommandPath as nt,resolveWindowsExecInvocation as rt}from"../../core/runtime/spawn.js";import{chmodSync as ne,readdirSync as re,readFileSync as j,realpathSync as Ee,rmSync as at,statSync as I,truncateSync as ot,existsSync as C,openSync as lt,writeSync as ct,closeSync as dt,renameSync as ye,mkdirSync as _e,constants as Se,watch as ut}from"node:fs";import{appendFile as ht,mkdir as M,readFile as A,rm as we,stat as Pe,writeFile as O}from"node:fs/promises";import{dirname as ft,isAbsolute as pt,join as f,resolve as mt}from"node:path";import{homedir as b,tmpdir as vt}from"node:os";import ae from"node:net";import{EventEmitter as Ce}from"node:events";import{fileURLToPath as gt}from"node:url";let W=null;if(process.platform==="win32")try{W=await import("node-pty")}catch{W=null}import{resolveRuntimePaths as _}from"../../core/config/index.js";import{normalizeClaudeEffort as Et,normalizeClaudeEnvEffort as yt}from"../../core/config/claude-direct-env.js";import{SESSION_MODE_IDS as q}from"./protocol-contract.js";import{buildSessionContextBlock as _t}from"../shared/session-context.js";import{log as o}from"../../core/log/index.js";import{ActivityStatusManager as St}from"./activity-status-manager.js";import{HookSignalStore as wt}from"../../core/hooks/hook-signal-store.js";import{QuestionStore as Te}from"../../core/persistence/question-store.js";import{PermissionStore as G}from"../../core/persistence/permission-store.js";import{InternalApiServer as Pt}from"../../core/mcp/internal-api-server.js";import{executeEventTool as Ct,isEventTool as Tt}from"../../core/mcp/event-tool-executor.js";import{validateToolArgs as At}from"../../core/mcp/tool-schemas.js";import{isGrixInternalToolName as kt,normalizeEventToolArgs as It}from"../../core/mcp/tools.js";import{scanSkills as bt}from"./skill-scanner.js";import{syncDefaultSkillsToDir as Rt}from"../../default-skills/index.js";import{clearSessionJsonlDriftCache as $t,countPendingBackgroundTasks as Ot,noteSessionJsonlPath as Lt,extractLastAssistantEntry as z,extractTextBlocksAfterOffset as Dt,hasTerminalToolResultAfterOffset as Ae,probeSessionTurnState as oe,resolveSessionJsonlPath as B}from"./usage-parser.js";import{readSettingsEnv as xt}from"./model-list.js";import{resolveCliPath as Mt,getCliVersion as Ft}from"../../core/util/cli-probe.js";import{GRIX_RELAY_ANTHROPIC_API_KEY as ke}from"../../core/proxy/index.js";const F="grix",Q={developmentChannels:!0,maxTurns:!0};function Nt(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 Ie(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,a=`Question ${s+1}`,l=String(r.header??r.question??a).trim()||a,c=String(r.prompt??r.question??"").trim();if(!c)continue;const u=Nt(r.options),h=r.multiSelect===!0||r.multi_select===!0;i.push({header:l,prompt:c,...u?{options:u}:{},...h?{multi_select:!0}:{}})}return i.length>0?i:null}function jt(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 Bt=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],Ut=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],Ht=/API Error:\s*400.*(server_tool_use\.id|messages\.\d+\.content)/,be=300*1e3,Re=300*1e3,$e=Re+30*1e3,Oe=3e3,qt=50,Gt=1800*1e3,zt=["CLAUDE_CODE_ENABLE_TELEMETRY","CLAUDE_CODE_ENHANCED_TELEMETRY_BETA","OTEL_LOGS_EXPORTER","OTEL_TRACES_EXPORTER","OTEL_METRICS_EXPORTER","OTEL_EXPORTER_OTLP_PROTOCOL","OTEL_EXPORTER_OTLP_ENDPOINT","OTEL_EXPORTER_OTLP_HEADERS","OTEL_LOGS_EXPORT_INTERVAL","OTEL_TRACES_EXPORT_INTERVAL","OTEL_LOG_USER_PROMPTS","OTEL_LOG_ASSISTANT_RESPONSES","OTEL_LOG_TOOL_DETAILS","OTEL_LOG_TOOL_CONTENT","OTEL_LOG_RAW_API_BODIES"];function Y(d=process.env){const e={...d},t=new Set(zt.map(i=>i.toLowerCase()));for(const i of Object.keys(e))t.has(i.toLowerCase())&&delete e[i];return e}const Le=60*1e3,De="lost injection respawn",xe=600*1e3,Jt=250;function Me(d,e){const t=Number(process.env[d]);return Number.isFinite(t)&&t>0?t:e}function Fe(d){return d.trim().replace(/[^a-zA-Z0-9._-]+/gu,"_")||"unknown"}function Wt(d){const e=nt("expect",d);return pt(e)?e:null}function K(d){try{return C(d)?I(d).size:0}catch{return 0}}function Ne(d){const e=[],t=i=>{let s;try{s=re(i,{withFileTypes:!0})}catch{return}for(const n of s){const r=f(i,n.name);n.isDirectory()?t(r):n.isFile()&&e.push(r)}};return t(d),e.sort((i,s)=>je(i)-je(s)||i.localeCompare(s))}function je(d){try{return I(d).mtimeMs}catch{return 0}}const Be=Me("GRIX_RESCUE_SETTLE_POLL_MS",2e3),Ue=Me("GRIX_RESCUE_SETTLE_BUDGET_MS",12e4),Qt=300*1e3,Yt="The model's tool call could not be parsed (retry also failed).",Kt="[\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",Vt="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.",Xt=2*1e3,le=90*1e3,He=1800*1e3,qe=15*1e3,Zt=60*1e3,Ge=10*1e3,ei=30*1e3,ti=30*1e3;function ii(d){const e=d.tool_name||"";if(e==="mcp__grix__grix_dispatch_agent")return"\u540E\u53F0\u534F\u8C03\u5B50 agent";if(e.startsWith("mcp__grix__"))return"\u540E\u53F0\u8C03\u7528 Grix \u5DE5\u5177";if(e.startsWith("mcp__"))return"\u540E\u53F0\u8C03\u7528\u5DE5\u5177";switch(e){case"Bash":return"\u540E\u53F0\u6267\u884C\u547D\u4EE4";case"Read":case"Edit":case"Write":case"NotebookEdit":return"\u540E\u53F0\u8BFB\u5199\u6587\u4EF6";case"Task":return"\u540E\u53F0\u8FD0\u884C\u5B50\u4EFB\u52A1";default:return e?`\u540E\u53F0\u8FD0\u884C\uFF1A${e}`:"\u540E\u53F0\u4EFB\u52A1\u8FDB\u884C\u4E2D"}}function si(d){try{return process.kill(d,0),!0}catch(e){return e.code==="EPERM"}}const ni=12e4,ri=3e4,ai=6e4,oi=3e3,V=1e3,ze=45e3,li=18e3,ce=8192,ci=2e3,di=2e3,ui=/(do you want to proceed\?[\s\S]*?\d\.\s*yes)|(only you can approve)/i,hi=/(do you want to proceed)|(only you can approve)|(\d\.\s*yes)|(\d\.\s*no)|(esc to cancel)|(tab to amend)/i,fi=/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i,de="__grix_key__ ";let Je=!1;const ue=new Set;async function pi(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=ae.createServer();r.once("error",n),r.listen(0,"127.0.0.1",()=>{const a=r.address(),l=typeof a=="object"&&a?a.port:0;r.close(c=>{if(c){n(c);return}s(l)})})});if(i>0&&!e.has(i)&&!ue.has(i))return ue.add(i),i}throw new Error("\u65E0\u6CD5\u5206\u914D MCP \u901A\u77E5\u7AEF\u53E3")}function mi(d){d>0&&ue.delete(d)}const vi=["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.","Local transcript availability, session binding, recovery mode, and connector history-loss diagnostics are internal operational details. Do not mention them, ask the user to choose a recovery mode, or say that history/context was lost unless the user explicitly asks to debug that mechanism.",'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 and the one the connector quotes. Your plain text reaches the user too \u2014 it is not private scratch space, so it is a real channel for progress and for anything the user needs mid-turn; you just need not restate the full conclusion as plain text once it is in the reply. 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).","Make the reply tool call the last thing you do in a turn and put your whole conclusion inside it: once you have called reply, any further text you write in the same turn is dropped, not delivered. Progress notes go before the reply; the reply ends the turn.",'Never explain the delivery mechanics to the user or tell them you "already sent it via the reply tool" \u2014 that is internal plumbing they neither see nor care about. If the user says they did not receive or cannot read something, do not insist you already sent it; assume the delivery or the format failed and send it again, switching format when that is the likely cause (e.g. render a diagram to an image, strip code fences, upload a file).',"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)."];function gi(d,e,t){const i=[...vi];e&&i.unshift(_t(e,t));try{const s=d?.agentName?.trim().slice(0,200),n=d?.introduction?.trim().slice(0,2e3);if(s||n){const r=[s?`Your name is "${s}".`:"",n?`Your self-introduction: ${n}`:""].filter(Boolean).join(" ");i.unshift(r)}}catch{}return i.join(" ")}function We(d){return String(d??"").trim().toLowerCase()===q.approval?q.approval:q.fullAuto}class ls extends Ce{type="claude";config;bridgeCallbacks;mcpServerProcess=null;internalApi=null;claudeProcess=null;claudePty=null;singleTurnFallbackProcesses=new WeakSet;singleTurnFallbackPtys=new WeakSet;lostInjectionKilledProcesses=new WeakSet;lostInjectionKilledPtys=new WeakSet;singleTurnFallbackChild=null;spawnPromise=null;lifecycleVersion=0;sessionId="";alive=!1;stopped=!1;activeEvent=null;completedAuditBoundaries=new Map;activeEventIdleTimer=null;activeEventHardTimer=null;activeEventPostReplyTimer=null;activeEventPostReplyWatcher=null;activeEventPostReplyPoll=null;rescueSettleTimer=null;stopHookBarrierSessionId=null;stopHookBarrierTimer=null;selfDrivenActive=!1;selfDrivenLastSignalAt=0;selfDrivenSweepTimer=null;selfDrivenReported=!1;selfDrivenReportTimer=null;selfDrivenLabel=null;compacting=!1;compactingTimer=null;compactionDoneResolver=null;mcpServerReady=!1;mcpStartupFailureHandled=!1;mcpChannelBroken=!1;channelGateClosed=!1;startupChannelListening=!1;startupChannelListeningAt=0;startupDialogSeenAt=0;pendingMcpFailureTimer=null;sessionIdConflictDetected=!1;sessionIdConflictRetriedEventIds=new Set;malformedToolRetriedEventIds=new Set;lostInjectionRetriedEventIds=new Set;runtimeResolver=null;activityManager=null;deferredModelId=null;claudeCliCapabilities=null;claudeChildPid=0;claudeCliSessionId="";claudeSessionCwd="";claudeMcpConfigPath="";expectRunDir="";auditEvidencePaths=null;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 Pt,this.internalApi.setInvokeHandler(async(e,t,i,s)=>this.handleInternalInvoke(e,t,i,s)),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,o.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(o.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.claudeCliSessionId&&$t(this.claudeCliSessionId),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.startupDialogSeenAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null),this.terminalPermissionKeyTimer&&(clearTimeout(this.terminalPermissionKeyTimer),this.terminalPermissionKeyTimer=null),this.terminalPermissionPromptTail="",this.terminalPermissionPromptVisible=!1,this.pendingPermissions.size>0){const s=_(),n=new G(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();we(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&&(H(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{}H(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.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 Ei(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&&!si(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,backgroundBusy:this.selfDrivenReported,sessions:0,details:this.sessionState?{sessionState:this.sessionState}:void 0}}getActiveEventIds(){return this.activeEvent?[this.activeEvent.eventId]:[]}takeAuditBoundary(e){const t=this.completedAuditBoundaries.get(e);return t&&this.completedAuditBoundaries.delete(e),t}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 st(e,t)}async probe(e){const t=this.config.command||"claude",i=await Mt(t),s=i!==null;let n=null,r;if(s){const h=await Ft(t);n=h.version,h.error&&(r=h.error)}else r={code:"cli_not_found",message:`command not found: ${t}`};const l=(xt().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 h=Date.now();try{const p=it(tt),m=["-p","ping","--output-format","json"];this.getClaudeCliCapabilities(t).maxTurns&&m.push("--max-turns","1");const v=rt(t,m);await p(v.command,v.args,{timeout:e.timeoutMs??8e3,encoding:"utf-8",windowsVerbatimArguments:v.windowsVerbatimArguments,env:Y()}),u={attempted:!0,ok:!0,latency_ms:Date.now()-h}}catch(p){const m=p,v=m.killed?"conversation_timeout":"conversation_failed";u={attempted:!0,ok:!1,latency_ms:Date.now()-h,error:{code:v,message:m.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=B(e,this.claudeSessionCwd);try{const i=I(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=bt({mode:"claude",projectDir:this.claudeSessionCwd}),r=n.map(a=>{const l=a.trigger?` (${a.trigger})`:"",c=a.pluginName?` [plugin:${a.pluginName}]`:` [${a.source}]`;return`- ${a.name}${l}${c}: ${a.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"};try{await this.waitForChannelReady()}catch(n){return{status:"failed",message:`Claude channel not ready for compact: ${n instanceof Error?n.message:n}`}}}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`):this.expectRunDir?this.writeExpectFifoLine(this.expectRunDir,s):this.claudeProcess?.stdin?.write(`${s}
|
|
3
|
-
`),e==="compact"){const n=new Promise(a=>{this.compactionDoneResolver=a});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}`}}}writeExpectFifoLine(e,t){const i=f(e,"cmd.fifo");let s;try{s=
|
|
4
|
-
`)}finally{ct(s)}}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?S(t.used_percentage):null,remainingPercentage:t?.remaining_percentage!=null?S(t.remaining_percentage):null,totalInputTokens:S(t?.total_input_tokens),totalOutputTokens:S(t?.total_output_tokens),contextWindowSize:S(t?.context_window_size)||2e5,currentUsage:r?{inputTokens:S(r.input_tokens),outputTokens:S(r.output_tokens),cacheCreationInputTokens:S(r.cache_creation_input_tokens),cacheReadInputTokens:S(r.cache_read_input_tokens)}:null},cost:{totalCostUsd:S(i?.total_cost_usd),totalDurationMs:S(i?.total_duration_ms),totalApiDurationMs:S(i?.total_api_duration_ms),totalLinesAdded:S(i?.total_lines_added),totalLinesRemoved:S(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 a=this.sessionState.rateLimits?.fiveHour,c=this.sessionState.rateLimits?.sevenDay;o.info("claude-adapter",`[rate-limits] statusLine parsed: fiveHour=${a?`${a.usedPercentage}% resetsAt=${a.resetsAt}`:"n/a"} sevenDay=${c?`${c.usedPercentage}% resetsAt=${c.resetsAt}`:"n/a"}`)}else o.debug("claude-adapter","[rate-limits] statusLine: no rate_limits in payload");try{this.bridgeCallbacks.onStatusLineUpdated?.(this.sessionState)}catch(a){o.warn("claude-adapter",`onStatusLineUpdated callback error: ${a instanceof Error?a.message:a}`)}}catch(t){o.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,a=S(r.used_percentage??r.usedPercent),c=S(r.resets_at??r.resetsAt);if(!(a<=0&&c<=0))return{usedPercentage:a,resetsAt:c}},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)){o.info("claude-adapter",`Event ${e.event_id} rejected: duplicate`),this.bridgeCallbacks.sendEventResult(e.event_id,"responded","duplicate event");return}if(Date.now()<this.authFailureUntil){o.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){o.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){o.info("claude-adapter",`Event ${e.event_id} ignored: same as active event`);return}if(this.activeEvent||this.compacting||this.stopHookBarrierSessionId===e.session_id){o.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){o.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;o.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:J()}),(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??""),a={...e,action_type:r==="allow"?"exec_approve":"exec_reject",params:{...i,approval_command_id:i.request_id}};return this.handlePermissionApproval(a)}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 o.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 Te(s.questionRequestsDir),r=await n.getRequest(i),a=Number(r?.created_at??0),c=a>0&&Date.now()-a>$e;if(!r||r.resolved||c){o.warn("claude-adapter",`Question reply has no pending waiter: request_id=${i} exists=${!!r} resolved=${r?.resolved===!0} expired=${c}`);const v=this.pendingQuestion.get(i);return v&&this.activeEvent?.eventId===v.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"interaction_request_not_pending","The question is no longer pending; the reply was not delivered."),{handled:!0,kind:"question_reply_not_pending"}}const l=t.resolution??{},u=String(l.type??""),h=l;if(u==="action"&&String(l.value??"")==="cancel"){o.info("claude-adapter",`Question cancelled by user: request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,h);const E=this.pendingQuestion.get(i);return E&&this.activeEvent?.eventId===E.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 p="";if(u==="text"?p=String(l.value??""):u==="map"&&(p=(Array.isArray(l.entries)?l.entries:[]).map(E=>E.value).join(", ")),!p){o.warn("claude-adapter",`Empty answer for question reply request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,h);const v=this.pendingQuestion.get(i);return v&&this.activeEvent?.eventId===v.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"}}o.info("claude-adapter",`Resolving question in store: request_id=${i} answer=${p.slice(0,80)}`),await n.resolveRequest(i,"answer",p,h);const m=this.pendingQuestion.get(i);return m&&this.activeEvent?.eventId===m.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=_(),a=new G(r.permissionRequestsDir),c=s?"allow":"deny";return await a.resolveRequest(i,c),this.pendingPermissions.delete(i),this.scheduleTerminalPermissionKey(i,c),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok"),o.info("claude-adapter",`Permission ${c}: approvalId=${i}`),{handled:!0,kind:"permission_approval"}}async handleExecApprovalEvent(e){const t=String(e.approval_id??e.approval_command_id??e.tool_call_id??"");if(!t)return{delivered:!1,errorCode:"approval_id_required",errorMsg:"approval_id is required"};if(!this.pendingPermissions.get(t))return{delivered:!1,errorCode:"approval_not_found",errorMsg:`no pending permission for approval_id: ${t}`};const i=e.decision==="deny"?"deny":"allow",s=_();return await new G(s.permissionRequestsDir).resolveRequest(t,i),this.pendingPermissions.delete(t),this.scheduleTerminalPermissionKey(t,i),o.info("claude-adapter",`Permission ${i} (text event): approvalId=${t}`),{delivered:!0}}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(!this.getClaudeCliCapabilities(this.config.command||"claude").developmentChannels){await this.runSingleTurnFallback(e,"claude CLI does not support development channels");return}if(await this.ensureClaudeProcessReady(),await this.waitForChannelReady(),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",this.bridgeCallbacks.getAgentId?.()],["attachments_json",e.attachments_json],["context_messages_json",We(e.context_messages_json)??e.context_messages_json]];for(const[a,c]of n)c!=null&&c!==""&&(s[a]=c);this.captureEventJsonlBaseOffset(e.event_id);const r=e.content??"";this.pushNotification("notifications/claude/channel",{content:r,meta:s}),this.activeEvent?.eventId===e.event_id&&(this.activeEvent.channelPushedAt=Date.now()),o.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 o.warn("claude-adapter",`Spawn failed (attempt ${t+1}/4), retrying: ${s}`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),he(this.claudeCliSessionId,[]),await new Promise(a=>setTimeout(a,(t+1)*2e3)),this.ensureClaudeAndPushEvent(e,t+1);o.error("claude-adapter",`Failed to deliver event ${e.event_id}: ${s}`);let r=s;if(this.shouldFallbackToSingleTurn(s)){o.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(a){o.error("claude-adapter",`Single-turn fallback failed for ${e.event_id}: ${a instanceof Error?a.message:String(a)}`),r=a}}this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"failed",r instanceof Error?r.message:String(r)),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("does not support development channels")||t.includes("unknown option")||t.includes("unknown or unexpected option")||t.includes("mcp stdio server not available")||/notify port\b.*\bnot ready/.test(t)||t.includes("file not found:"):!1}getClaudeCliCapabilities(e){return this.claudeCliCapabilities?this.claudeCliCapabilities:(this.claudeCliCapabilities=Ai(e),this.claudeCliCapabilities)}buildSingleTurnPrompt(e,t=!0){const i=t&&e.context_messages_json?We(e.context_messages_json)??String(e.context_messages_json):"",s=[e.content??""];return i&&(s.push("","Conversation context (JSON):"),s.push(i)),s.join(`
|
|
5
|
-
`)}extractSingleTurnDelta(e){const t=e.trim();if(!t)return null;let i;try{i=JSON.parse(t)}catch{return o.warn("claude-adapter",`Single-turn fallback: dropping non-JSON stdout noise: ${t.slice(0,200)}`),null}const s=i.type==="stream_event"&&i.event&&typeof i.event=="object"?i.event:i,n=String(s.type??"");if(n==="content_block_delta"){const r=s.delta;if(r&&typeof r=="object"){const a=String(r.text??"");if(a)return{text:a,final:!1}}return null}if(n==="result"){const r=String(s.subtype??""),a=String(s.terminal_reason??""),
|
|
6
|
-
`,{encoding:"utf8",mode:384}),ne(m,384)}),this.auditEvidencePaths=
|
|
7
|
-
`);for(;T>=0;){const U=
|
|
8
|
-
`)}}),k.stderr?.setEncoding("utf8"),k.stderr?.on("data",$=>{y.length>=ce||(y+=$,y.length>ce&&(y=y.slice(0,ce)))}),k.on("error",$=>{P(),g($)}),k.on("close",$=>{if(P(),D.trim()&&ge(D),ee){g(ee);return}if($!==0&&!x){const T=y.trim();g(new Error(T||`claude single-turn exited with code ${$}`));return}if(!x&&this.activeEvent?.eventId===e.event_id&&!this.isEventAnswered()){const T=y.trim();g(new Error(T||"claude single-turn produced no output"));return}x&&(R+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",R,!0,me,ve)),L()})}),this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"responded"),this.clearActiveEvent())}async waitForChannelReady(){if(!this.mcpServerReady)throw new Error("MCP stdio server not available");await this.waitForNotifyPort(3e4),process.platform==="win32"?await this.waitForWindowsChannelReady(Ge):await this.waitForChannelListening(Ge)}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??[],a=Je(s.modeId),c=String(s.modelId??"").trim(),l=String(s.cwd??"").trim();this.claudeSessionCwd=l;const u=String(s.claudeSessionId??"").trim()||J();this.claudeCliSessionId=u,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(u);const h=this.buildClaudeRuntimeEnv((()=>{const w=this.resolveEventAuditConfig(s,this.activeEvent?.rawEvent);return w?{...s,audit:w}:s})());let p=Ke(u,l||void 0);if(p&&Xe(u,l||void 0)&&(p=!1),String(s.claudeSessionId??"").trim()!==""&&!p&&o.warn("claude-adapter",`Claude local transcript missing for session ${u}; continuing silently with platform context`),p?Ze(u,l||void 0):(he(u,[]),fe(u)),this.runtimeResolver&&!l)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!l)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(l),await this.ensureClaudeOnboardingFlags(l),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(l);let m=!1,v="",E=null;try{t(),this.notifyPort=await fi([this.internalApiPort]),m=!0,t(),o.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const w=i.pluginDir||await this.ensureClaudePluginDir(),L=p?["--resume",u]:["--session-id",u],g=[...r,"--name",`grix-${this.sessionId}`,...L,"--plugin-dir",w,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath];c&&(/^claude/i.test(c)?g.push("--model",c):(o.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)),a===q.fullAuto&&g.push("--dangerously-skip-permissions"),g.push("--dangerously-load-development-channels",`server:${F}`);const k=vi(this.bridgeCallbacks.getAgentProfile?.(),p?void 0:this.sessionId,this.bridgeCallbacks.getAgentId?.());if(process.platform==="win32"){const P=k.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),y=Ti(this.claudeCliSessionId);await Ii(y,P),g.push("--append-system-prompt-file",y)}else g.push("--append-system-prompt",k);if(process.platform==="win32")if(W){const P=Ri(),y=["/c",n,...g];o.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${l} mode=${a} ${p?"resume":"new"} ${n} ${g.join(" ")}`);const R=W.spawn(P,y,{name:"xterm-256color",cols:120,rows:30,cwd:l,env:h,useConpty:!0,conptyInheritCursor:!1});this.claudePty=R,this.claudeChildPid=R.pid,this.startPtyAutoConfirm(R),E=null}else o.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${l} mode=${a} ${p?"resume":"new"} ${n} ${g.join(" ")}`),E=te(n,g,{cwd:l,env:h,stdio:["pipe","pipe","pipe"],detached:process.platform!=="win32",shell:!0,windowsHide:!0});else{o.info("claude-adapter",`Spawning via expect PTY: cwd=${l} mode=${a} ${p?"resume":"new"} ${n} ${g.join(" ")}`);const P=Jt(typeof h.PATH=="string"?h.PATH:void 0);if(!P)throw new Error("expect not found in PATH. Install it with: apt install expect / dnf install expect / brew install expect");const y=f(mt(),`grix-claude-${J()}`);await M(y,{recursive:!0}),this.expectRunDir=y;const{expectPath:R,pidPath:x}=await Si(y,n,g);v=x,t(),E=te(P,[R],{cwd:l,env:h,stdio:["pipe","pipe","pipe"],detached:!0})}if(this.claudeProcess=E,this.clearPendingMcpFailureTimer(),this.mcpStartupFailureHandled=!1,this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.startupDialogSeenAt=0,E&&E.on("error",P=>{o.error("claude-adapter","Claude process spawn error: "+P),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=E?.pid??0;else{const P=await Pi(E,v);if(t(),!Number.isFinite(P)||P<=0)throw new Error("failed to determine spawned Claude pid");this.claudeChildPid=P}if(!this.claudeChildPid||this.claudeChildPid<=0)throw new Error("failed to determine spawned Claude pid")}catch(w){if(E?.pid&&H(E,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===E&&(this.claudeProcess=null),this.claudeChildPid=0,m&&this.releaseNotifyPortReservation(),this.stopMcpServer(),w}const N=this.claudeChildPid;o.info("claude-adapter","Claude child PID: "+N+(this.claudePty?" (via PTY)":"")),this.setupProcessOrPtyHandlers(),this.alive=!0}setupProcessOrPtyHandlers(){if(this.claudeProcess){const e=this.claudeProcess;e.on("exit",(t,i)=>{if(o.info("claude-adapter",`Claude process exited (code=${t}, signal=${i})`),this.singleTurnFallbackProcesses.delete(e)){o.info("claude-adapter","Ignoring stale Claude process exit after lifecycle ownership transfer");return}if(this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const s=this.isEventAnswered(),n=s?"responded":"failed",r=s?void 0:`Claude process exited before completing its reply (code=${t}, signal=${i})`;o.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, answered=${s}, sending ${n}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,n,r),this.clearActiveEvent()}this.stopped||this.emit("exit",t),this.compacting&&this.finishCompaction("process-exit")}}),e.stdout?.on("data",t=>{this.claudeProcess===e&&this.handleClaudeOutput(t.toString())}),e.stderr?.on("data",t=>{if(this.claudeProcess!==e)return;const i=t.toString().trim();i&&o.info("claude-adapter",`[claude stderr] ${i}`),this.checkFailurePatterns(i)})}if(this.claudePty){const e=this.claudePty;e.onExit(({exitCode:t,signal:i})=>{if(o.info("claude-adapter",`Claude PTY process exited (code=${t}, signal=${i})`),this.singleTurnFallbackPtys.delete(e)){o.info("claude-adapter","Ignoring stale Claude PTY exit after lifecycle ownership transfer");return}if(this.claudePty=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const s=this.isEventAnswered(),n=s?"responded":"failed",r=s?void 0:`Claude process exited before completing its reply (code=${t}, signal=${i})`;o.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, answered=${s}, sending ${n}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,n,r),this.clearActiveEvent()}this.stopped||this.emit("exit",t),this.compacting&&this.finishCompaction("process-exit")}}),e.onData(t=>{this.claudePty===e&&(this.handleClaudeOutput(t),this.checkFailurePatterns(t))})}}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",a=this.ptyProbe.lastMeaningfulAt?t-this.ptyProbe.lastMeaningfulAt:-1;o.debug("claude-adapter",`[pty-probe] session=${this.sessionId??"?"} event=${r} win2s title=${this.ptyProbe.title} spinner=${this.ptyProbe.spinner} meaningful=${this.ptyProbe.meaningful} meaningfulGapMs=${a}`),this.ptyProbe.title=0,this.ptyProbe.spinner=0,this.ptyProbe.meaningful=0,this.ptyProbe.lastLogAt=t}}terminalPermissionPromptTail="";terminalPermissionPromptVisible=!1;terminalPermissionKeyTimer=null;trackTerminalPermissionPrompt(e){const t=e.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g," ").replace(/[\x00-\x1f\x7f-\x9f]/g," ").trim();if(t){if(ui.test(t)){this.terminalPermissionPromptTail=`${this.terminalPermissionPromptTail} ${t}`.slice(-ci),di.test(this.terminalPermissionPromptTail)&&(this.terminalPermissionPromptVisible=!0);return}t.length<=3||hi.test(t)||(this.terminalPermissionPromptTail="",this.terminalPermissionPromptVisible=!1)}}sendTerminalKey(e){const t=e==="enter"?"\r":"\x1B";try{if(this.claudePty)return this.claudePty.write(t),!0;if(this.expectRunDir)return this.writeExpectFifoLine(this.expectRunDir,`${de}${e}`),!0}catch(i){o.warn("claude-adapter",`Failed to send terminal key ${e}: ${i instanceof Error?i.message:i}`)}return!1}scheduleTerminalPermissionKey(e,t){this.terminalPermissionKeyTimer&&clearTimeout(this.terminalPermissionKeyTimer),this.terminalPermissionKeyTimer=setTimeout(()=>{if(this.terminalPermissionKeyTimer=null,this.stopped||!this.terminalPermissionPromptVisible)return;const i=t==="allow"?"enter":"esc";this.sendTerminalKey(i)&&(this.terminalPermissionPromptTail="",this.terminalPermissionPromptVisible=!1,o.info("claude-adapter",`Permission prompt still on terminal after ${t}, sent ${i} key: approvalId=${e}`))},li),this.terminalPermissionKeyTimer.unref()}handleClaudeOutput(e){this.recordPtyProbe(e),this.trackTerminalPermissionPrompt(e),this.compacting&&this.resetCompactingActivityTimer();const t=e.trim();if(t){const a=t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202f\ufeff]/g,"").trim();a?a.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(a)?o.debug("claude-adapter",`[claude output] ${a}`):o.info("claude-adapter",`[claude output] ${a.slice(0,500)}`):o.debug("claude-adapter",`[claude output] ${t.slice(0,200).replace(/[^\x20-\x7e]/g,c=>`\\x${c.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 a=i[1];this.sessionIdConflictDetected=!0;const c=[this.claudeProcess?.pid,this.claudeChildPid,this.claudePty?.pid].filter(l=>!!l&&l>0);he(a,c)}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)&&(o.info("claude-adapter","Auto-accepting workspace trust dialog"),this.claudePty.write("1\r")),!this.startupDialogSeenAt&&!this.startupChannelListening&&/loadingdevelopmentchannels|usingthisforlocaldevelopment/.test(n)&&(this.startupDialogSeenAt=Date.now(),o.info("claude-adapter","Dev channels dialog rendered; restarting notify port wait window")),(/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,o.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){o.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()},ai))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),jt.some(t=>t.test(e))&&(o.error("claude-adapter",`Auth failure: ${e}`),this.authFailureUntil=Date.now()+Ie,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
|
|
3
|
+
`),e==="compact"){const n=new Promise(a=>{this.compactionDoneResolver=a});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}`}}}writeExpectFifoLine(e,t){const i=f(e,"cmd.fifo");let s;try{s=lt(i,Se.O_WRONLY|Se.O_NONBLOCK)}catch(n){const r=n.code;throw new Error(r==="ENXIO"?"expect process is not running \u2014 cannot inject command":`FIFO open failed: ${n instanceof Error?n.message:n}`)}try{ct(s,`${t}
|
|
4
|
+
`)}finally{dt(s)}}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?S(t.used_percentage):null,remainingPercentage:t?.remaining_percentage!=null?S(t.remaining_percentage):null,totalInputTokens:S(t?.total_input_tokens),totalOutputTokens:S(t?.total_output_tokens),contextWindowSize:S(t?.context_window_size)||2e5,currentUsage:r?{inputTokens:S(r.input_tokens),outputTokens:S(r.output_tokens),cacheCreationInputTokens:S(r.cache_creation_input_tokens),cacheReadInputTokens:S(r.cache_read_input_tokens)}:null},cost:{totalCostUsd:S(i?.total_cost_usd),totalDurationMs:S(i?.total_duration_ms),totalApiDurationMs:S(i?.total_api_duration_ms),totalLinesAdded:S(i?.total_lines_added),totalLinesRemoved:S(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 a=this.sessionState.rateLimits?.fiveHour,l=this.sessionState.rateLimits?.sevenDay;o.info("claude-adapter",`[rate-limits] statusLine parsed: fiveHour=${a?`${a.usedPercentage}% resetsAt=${a.resetsAt}`:"n/a"} sevenDay=${l?`${l.usedPercentage}% resetsAt=${l.resetsAt}`:"n/a"}`)}else o.debug("claude-adapter","[rate-limits] statusLine: no rate_limits in payload");try{this.bridgeCallbacks.onStatusLineUpdated?.(this.sessionState)}catch(a){o.warn("claude-adapter",`onStatusLineUpdated callback error: ${a instanceof Error?a.message:a}`)}}catch(t){o.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,a=S(r.used_percentage??r.usedPercent),l=S(r.resets_at??r.resetsAt);if(!(a<=0&&l<=0))return{usedPercentage:a,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)){o.info("claude-adapter",`Event ${e.event_id} rejected: duplicate`),this.bridgeCallbacks.sendEventResult(e.event_id,"responded","duplicate event");return}if(Date.now()<this.authFailureUntil){o.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){o.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){o.info("claude-adapter",`Event ${e.event_id} ignored: same as active event`);return}if(this.activeEvent||this.compacting||this.stopHookBarrierSessionId===e.session_id){o.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){o.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;o.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:J()}),(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??""),a={...e,action_type:r==="allow"?"exec_approve":"exec_reject",params:{...i,approval_command_id:i.request_id}};return this.handlePermissionApproval(a)}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 o.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 Te(s.questionRequestsDir),r=await n.getRequest(i),a=Number(r?.created_at??0),l=a>0&&Date.now()-a>$e;if(!r||r.resolved||l){o.warn("claude-adapter",`Question reply has no pending waiter: request_id=${i} exists=${!!r} resolved=${r?.resolved===!0} expired=${l}`);const v=this.pendingQuestion.get(i);return v&&this.activeEvent?.eventId===v.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(i),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"interaction_request_not_pending","The question is no longer pending; the reply was not delivered."),{handled:!0,kind:"question_reply_not_pending"}}const c=t.resolution??{},u=String(c.type??""),h=c;if(u==="action"&&String(c.value??"")==="cancel"){o.info("claude-adapter",`Question cancelled by user: request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,h);const E=this.pendingQuestion.get(i);return E&&this.activeEvent?.eventId===E.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 p="";if(u==="text"?p=String(c.value??""):u==="map"&&(p=(Array.isArray(c.entries)?c.entries:[]).map(E=>E.value).join(", ")),!p){o.warn("claude-adapter",`Empty answer for question reply request_id=${i}`),await n.resolveRequest(i,"cancel",void 0,h);const v=this.pendingQuestion.get(i);return v&&this.activeEvent?.eventId===v.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"}}o.info("claude-adapter",`Resolving question in store: request_id=${i} answer=${p.slice(0,80)}`),await n.resolveRequest(i,"answer",p,h);const m=this.pendingQuestion.get(i);return m&&this.activeEvent?.eventId===m.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=_(),a=new G(r.permissionRequestsDir),l=s?"allow":"deny";return await a.resolveRequest(i,l),this.pendingPermissions.delete(i),this.scheduleTerminalPermissionKey(i,l),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok"),o.info("claude-adapter",`Permission ${l}: approvalId=${i}`),{handled:!0,kind:"permission_approval"}}async handleExecApprovalEvent(e){const t=String(e.approval_id??e.approval_command_id??e.tool_call_id??"");if(!t)return{delivered:!1,errorCode:"approval_id_required",errorMsg:"approval_id is required"};if(!this.pendingPermissions.get(t))return{delivered:!1,errorCode:"approval_not_found",errorMsg:`no pending permission for approval_id: ${t}`};const i=e.decision==="deny"?"deny":"allow",s=_();return await new G(s.permissionRequestsDir).resolveRequest(t,i),this.pendingPermissions.delete(t),this.scheduleTerminalPermissionKey(t,i),o.info("claude-adapter",`Permission ${i} (text event): approvalId=${t}`),{delivered:!0}}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(!this.getClaudeCliCapabilities(this.config.command||"claude").developmentChannels){await this.runSingleTurnFallback(e,"claude CLI does not support development channels");return}if(await this.ensureClaudeProcessReady(),await this.waitForChannelReady(),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",this.bridgeCallbacks.getAgentId?.()],["attachments_json",e.attachments_json],["context_messages_json",Qe(e.context_messages_json)??e.context_messages_json]];for(const[a,l]of n)l!=null&&l!==""&&(s[a]=l);this.captureEventJsonlBaseOffset(e.event_id);const r=e.content??"";this.pushNotification("notifications/claude/channel",{content:r,meta:s}),this.activeEvent?.eventId===e.event_id&&(this.activeEvent.channelPushedAt=Date.now()),o.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 o.warn("claude-adapter",`Spawn failed (attempt ${t+1}/4), retrying: ${s}`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),he(this.claudeCliSessionId,[]),await new Promise(a=>setTimeout(a,(t+1)*2e3)),this.ensureClaudeAndPushEvent(e,t+1);o.error("claude-adapter",`Failed to deliver event ${e.event_id}: ${s}`);let r=s;if(this.shouldFallbackToSingleTurn(s)){o.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(a){o.error("claude-adapter",`Single-turn fallback failed for ${e.event_id}: ${a instanceof Error?a.message:String(a)}`),r=a}}this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"failed",r instanceof Error?r.message:String(r)),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("does not support development channels")||t.includes("unknown option")||t.includes("unknown or unexpected option")||t.includes("mcp stdio server not available")||/notify port\b.*\bnot ready/.test(t)||t.includes("file not found:"):!1}getClaudeCliCapabilities(e){return this.claudeCliCapabilities?this.claudeCliCapabilities:(this.claudeCliCapabilities=ki(e),this.claudeCliCapabilities)}buildSingleTurnPrompt(e,t=!0){const i=t&&e.context_messages_json?Qe(e.context_messages_json)??String(e.context_messages_json):"",s=[e.content??""];return i&&(s.push("","Conversation context (JSON):"),s.push(i)),s.join(`
|
|
5
|
+
`)}extractSingleTurnDelta(e){const t=e.trim();if(!t)return null;let i;try{i=JSON.parse(t)}catch{return o.warn("claude-adapter",`Single-turn fallback: dropping non-JSON stdout noise: ${t.slice(0,200)}`),null}const s=i.type==="stream_event"&&i.event&&typeof i.event=="object"?i.event:i,n=String(s.type??"");if(n==="content_block_delta"){const r=s.delta;if(r&&typeof r=="object"){const a=String(r.text??"");if(a)return{text:a,final:!1}}return null}if(n==="result"){const r=String(s.subtype??""),a=String(s.terminal_reason??""),l=s.is_error===!0||a==="api_error";if(r&&r!=="success"||l){const u=Array.isArray(s.errors)?s.errors.join("; "):"",h=String(s.result??"").trim(),p=u||h||a;throw new Error(`claude single-turn result ${r||"error"}${p?`: ${p}`:""}`)}const c=String(s.result??"");return c?{text:c,final:!0}:null}return null}resolveEventAuditConfig(e,t){if(e.audit?.enabled===!0)return e.audit;const i=t?.audit;if(i?.enabled!==!0)return;const s=i.profile??"full";return{enabled:!0,profile:s,capture:i.capture??(s==="usage"?{inputOutput:!1,tools:!1,subagents:!0,thinking:!1,rawProviderBody:i.rawProviderBody===!0}:{inputOutput:!0,tools:!0,subagents:!0,thinking:!1,rawProviderBody:i.rawProviderBody===!0})}}buildClaudeRuntimeEnv(e=this.resolveSessionRuntime()){const t={...Y(),...this.config.env};if(delete t.CLAUDECODE,t.CLAUDE_PLUGIN_DATA=_().dataDir,t.GRIX_HOOK_SIGNALS_PATH=this.resolveHookSignalsPath(),this.internalApi&&(t.GRIX_CLAUDE_INTERNAL_API_URL=this.internalApi.baseUrl,t.GRIX_CLAUDE_INTERNAL_API_TOKEN=this.internalApi.token),e.audit?.enabled===!0&&!this.internalApi&&o.error("claude-adapter","Audit enabled but internal API not started; OTEL capture env will not be injected"),e.audit?.enabled===!0&&this.internalApi){const s=this.ensureClaudeAuditEvidence(e.audit);t.CLAUDE_CODE_ENABLE_TELEMETRY="1",t.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA="1",t.OTEL_LOGS_EXPORTER="otlp",t.OTEL_TRACES_EXPORTER="otlp",t.OTEL_METRICS_EXPORTER="none",t.OTEL_EXPORTER_OTLP_PROTOCOL="http/json",t.OTEL_EXPORTER_OTLP_ENDPOINT=this.internalApi.baseUrl,t.OTEL_EXPORTER_OTLP_HEADERS=`Authorization=Bearer ${this.internalApi.token}`,t.OTEL_LOGS_EXPORT_INTERVAL="1000",t.OTEL_TRACES_EXPORT_INTERVAL="1000",t.OTEL_LOG_USER_PROMPTS=e.audit.capture.inputOutput?"1":"0",t.OTEL_LOG_ASSISTANT_RESPONSES=e.audit.capture.inputOutput?"1":"0",t.OTEL_LOG_TOOL_DETAILS=e.audit.capture.tools?"1":"0",t.OTEL_LOG_TOOL_CONTENT=e.audit.capture.tools?"1":"0",e.audit.capture.rawProviderBody&&(t.OTEL_LOG_RAW_API_BODIES=`file:${s.rawBodyDir}`)}const i=Et(e.effort);if(i==="auto")delete t.CLAUDE_CODE_EFFORT_LEVEL;else{const s=yt(i);s&&(t.CLAUDE_CODE_EFFORT_LEVEL=s)}return t}ensureClaudeAuditEvidence(e){if(this.auditEvidencePaths)return this.auditEvidencePaths;const t=this.config.options??{},s=String(t.auditEvidenceRoot??"").trim()||f(_().dataDir,"audit-evidence"),n=Fe(this.sessionId),r=Fe(this.claudeCliSessionId||"pending"),a=f(s,"claude",n,r),l=f(a,"raw-api-bodies");_e(l,{recursive:!0,mode:448}),ne(a,448),ne(l,448);const c={rootDir:a,logsPath:f(a,"otel-logs.jsonl"),tracesPath:f(a,"otel-traces.jsonl"),rawBodyDir:l,rawProviderBody:e.capture.rawProviderBody};return this.internalApi?.setOtelHandler(async(u,h,p)=>{const m=u==="logs"?c.logsPath:c.tracesPath;await ht(m,`${JSON.stringify({observedAt:new Date().toISOString(),signal:u,payload:h,rawBody:p})}
|
|
6
|
+
`,{encoding:"utf8",mode:384}),ne(m,384)}),this.auditEvidencePaths=c,c}async runSingleTurnFallback(e,t){const i=this.resolveSessionRuntime(),s=this.resolveEventAuditConfig(i,e);if(s&&!this.internalApi){o.error("claude-adapter",`Audit capture unavailable for event ${e.event_id}: internal API not started`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","audit capture unavailable: internal API not started","audit_capture_unavailable");return}if(this.activeEvent?.eventId===e.event_id&&(this.activeEvent.singleTurnFallback=!0,s&&(this.activeEvent.auditSingleTurnFallback=!0)),this.claudeProcess||this.claudePty){const D=this.mcpChannelBroken,g=this.channelGateClosed;this.killClaudeProcess("single-turn fallback isolation"),this.mcpChannelBroken=D,this.channelGateClosed=g}const n=this.config.options??{},r=this.config.command||"claude",a=(this.config.args??[]).filter(D=>{const g=String(D).trim().toLowerCase();return g!=="--dangerously-load-development-channels"&&g!=="--session-id"&&g!=="--resume"}),l=String(i.cwd??"").trim(),c=String(i.modelId??"").trim(),u=We(i.modeId);await this.ensureWorkspaceTrust(l),await this.ensureClaudeOnboardingFlags(l),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(l);const h=String(i.claudeSessionId??"").trim()||this.claudeCliSessionId||J();this.claudeCliSessionId=h,this.claudeSessionCwd=l,!i.claudeSessionId&&i.onSessionIdAssigned&&i.onSessionIdAssigned(h),await this.ensureStdioMcpServer();const p=this.buildClaudeRuntimeEnv(s?{...i,audit:s}:i),m=i.pluginDir??n.pluginDir??await this.ensureClaudePluginDir();let v=Xe(h,l||void 0);v&&Ze(h,l||void 0)&&(v=!1);const E=String(i.claudeSessionId??"").trim()!==""&&!v;v&&et(h,l||void 0),this.captureSingleTurnFallbackJsonlBaseOffset(e.event_id),E&&o.warn("claude-adapter",`Claude local transcript missing for session ${h}; continuing silently with platform context`);const N=this.buildSingleTurnPrompt(e,!v),w=[...a,"-p","--output-format","stream-json","--verbose","--include-partial-messages",...v?["--resume",h]:["--session-id",h]];w.push("--plugin-dir",m,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath,"--permission-mode",u===q.fullAuto?"bypassPermissions":"default"),c&&w.push("--model",c),w.push(N),o.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((D,g)=>{const k=te(r,w,{cwd:l||process.cwd(),env:p,stdio:["ignore","pipe","pipe"],detached:process.platform!=="win32",windowsHide:!0});this.singleTurnFallbackChild=k;const P=()=>{this.singleTurnFallbackChild===k&&(this.singleTurnFallbackChild=null)};let y="",R=0,x=!1,ee=null;const me=`fallback_${e.event_id}_${Date.now()}`,ve=String(e.msg_id??"").trim()||void 0;let L="";const ge=$=>{let T;try{T=this.extractSingleTurnDelta($)}catch(U){ee=U instanceof Error?U:new Error(String(U));return}T&&(T.final&&x||this.activeEvent?.eventId===e.event_id&&this.isEventAnswered()||(R+=1,x=!0,this.markActiveEventActivity(e.event_id,e.session_id),this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,T.text,R,!1,me,ve)))};k.stdout?.setEncoding("utf8"),k.stdout?.on("data",$=>{L+=$;let T=L.indexOf(`
|
|
7
|
+
`);for(;T>=0;){const U=L.slice(0,T);L=L.slice(T+1),ge(U),T=L.indexOf(`
|
|
8
|
+
`)}}),k.stderr?.setEncoding("utf8"),k.stderr?.on("data",$=>{y.length>=ce||(y+=$,y.length>ce&&(y=y.slice(0,ce)))}),k.on("error",$=>{P(),g($)}),k.on("close",$=>{if(P(),L.trim()&&ge(L),ee){g(ee);return}if($!==0&&!x){const T=y.trim();g(new Error(T||`claude single-turn exited with code ${$}`));return}if(!x&&this.activeEvent?.eventId===e.event_id&&!this.isEventAnswered()){const T=y.trim();g(new Error(T||"claude single-turn produced no output"));return}x&&(R+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",R,!0,me,ve)),D()})}),this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"responded"),this.clearActiveEvent())}async waitForChannelReady(){if(!this.mcpServerReady)throw new Error("MCP stdio server not available");await this.waitForNotifyPort(3e4),process.platform==="win32"?await this.waitForWindowsChannelReady(ze):await this.waitForChannelListening(ze)}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??[],a=We(s.modeId),l=String(s.modelId??"").trim(),c=String(s.cwd??"").trim();this.claudeSessionCwd=c;const u=String(s.claudeSessionId??"").trim()||J();this.claudeCliSessionId=u,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(u);const h=this.buildClaudeRuntimeEnv((()=>{const w=this.resolveEventAuditConfig(s,this.activeEvent?.rawEvent);return w?{...s,audit:w}:s})());let p=Xe(u,c||void 0);if(p&&Ze(u,c||void 0)&&(p=!1),String(s.claudeSessionId??"").trim()!==""&&!p&&o.warn("claude-adapter",`Claude local transcript missing for session ${u}; continuing silently with platform context`),p?et(u,c||void 0):(he(u,[]),fe(u)),this.runtimeResolver&&!c)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!c)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(c),await this.ensureClaudeOnboardingFlags(c),await this.ensureSkipDangerousPermissionPrompt(),await this.injectStatusLineSettings(c);let m=!1,v="",E=null;try{t(),this.notifyPort=await pi([this.internalApiPort]),m=!0,t(),o.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),t();const w=i.pluginDir||await this.ensureClaudePluginDir(),D=p?["--resume",u]:["--session-id",u],g=[...r,"--name",`grix-${this.sessionId}`,...D,"--plugin-dir",w,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath];l&&(/^claude/i.test(l)?g.push("--model",l):(o.warn("claude-adapter",`Skipping --model ${l}: 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=l)),a===q.fullAuto&&g.push("--dangerously-skip-permissions"),g.push("--dangerously-load-development-channels",`server:${F}`);const k=gi(this.bridgeCallbacks.getAgentProfile?.(),p?void 0:this.sessionId,this.bridgeCallbacks.getAgentId?.());if(process.platform==="win32"){const P=k.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),y=Ai(this.claudeCliSessionId);await Ri(y,P),g.push("--append-system-prompt-file",y)}else g.push("--append-system-prompt",k);if(process.platform==="win32")if(W){const P=$i(),y=["/c",n,...g];o.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${c} mode=${a} ${p?"resume":"new"} ${n} ${g.join(" ")}`);const R=W.spawn(P,y,{name:"xterm-256color",cols:120,rows:30,cwd:c,env:h,useConpty:!0,conptyInheritCursor:!1});this.claudePty=R,this.claudeChildPid=R.pid,this.startPtyAutoConfirm(R),E=null}else o.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${c} mode=${a} ${p?"resume":"new"} ${n} ${g.join(" ")}`),E=te(n,g,{cwd:c,env:h,stdio:["pipe","pipe","pipe"],detached:process.platform!=="win32",shell:!0,windowsHide:!0});else{o.info("claude-adapter",`Spawning via expect PTY: cwd=${c} mode=${a} ${p?"resume":"new"} ${n} ${g.join(" ")}`);const P=Wt(typeof h.PATH=="string"?h.PATH:void 0);if(!P)throw new Error("expect not found in PATH. Install it with: apt install expect / dnf install expect / brew install expect");const y=f(vt(),`grix-claude-${J()}`);await M(y,{recursive:!0}),this.expectRunDir=y;const{expectPath:R,pidPath:x}=await wi(y,n,g);v=x,t(),E=te(P,[R],{cwd:c,env:h,stdio:["pipe","pipe","pipe"],detached:!0})}if(this.claudeProcess=E,this.clearPendingMcpFailureTimer(),this.mcpStartupFailureHandled=!1,this.mcpChannelBroken=!1,this.channelGateClosed=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.startupDialogSeenAt=0,E&&E.on("error",P=>{o.error("claude-adapter","Claude process spawn error: "+P),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=E?.pid??0;else{const P=await Ci(E,v);if(t(),!Number.isFinite(P)||P<=0)throw new Error("failed to determine spawned Claude pid");this.claudeChildPid=P}if(!this.claudeChildPid||this.claudeChildPid<=0)throw new Error("failed to determine spawned Claude pid")}catch(w){if(E?.pid&&H(E,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===E&&(this.claudeProcess=null),this.claudeChildPid=0,m&&this.releaseNotifyPortReservation(),this.stopMcpServer(),w}const N=this.claudeChildPid;o.info("claude-adapter","Claude child PID: "+N+(this.claudePty?" (via PTY)":"")),this.setupProcessOrPtyHandlers(),this.alive=!0}setupProcessOrPtyHandlers(){if(this.claudeProcess){const e=this.claudeProcess;e.on("exit",(t,i)=>{if(o.info("claude-adapter",`Claude process exited (code=${t}, signal=${i})`),this.singleTurnFallbackProcesses.delete(e)){o.info("claude-adapter","Ignoring stale Claude process exit after lifecycle ownership transfer");return}if(this.lostInjectionKilledProcesses.delete(e)){o.info("claude-adapter","Ignoring expected Claude process exit after lost-injection respawn");return}if(this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const s=this.isEventAnswered(),n=s?"responded":"failed",r=s?void 0:`Claude process exited before completing its reply (code=${t}, signal=${i})`;o.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, answered=${s}, sending ${n}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,n,r),this.clearActiveEvent()}this.stopped||this.emit("exit",t),this.compacting&&this.finishCompaction("process-exit")}}),e.stdout?.on("data",t=>{this.claudeProcess===e&&this.handleClaudeOutput(t.toString())}),e.stderr?.on("data",t=>{if(this.claudeProcess!==e)return;const i=t.toString().trim();i&&o.info("claude-adapter",`[claude stderr] ${i}`),this.checkFailurePatterns(i)})}if(this.claudePty){const e=this.claudePty;e.onExit(({exitCode:t,signal:i})=>{if(o.info("claude-adapter",`Claude PTY process exited (code=${t}, signal=${i})`),this.singleTurnFallbackPtys.delete(e)){o.info("claude-adapter","Ignoring stale Claude PTY exit after lifecycle ownership transfer");return}if(this.lostInjectionKilledPtys.delete(e)){o.info("claude-adapter","Ignoring expected Claude PTY exit after lost-injection respawn");return}if(this.claudePty=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const s=this.isEventAnswered(),n=s?"responded":"failed",r=s?void 0:`Claude process exited before completing its reply (code=${t}, signal=${i})`;o.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, answered=${s}, sending ${n}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,n,r),this.clearActiveEvent()}this.stopped||this.emit("exit",t),this.compacting&&this.finishCompaction("process-exit")}}),e.onData(t=>{this.claudePty===e&&(this.handleClaudeOutput(t),this.checkFailurePatterns(t))})}}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",a=this.ptyProbe.lastMeaningfulAt?t-this.ptyProbe.lastMeaningfulAt:-1;o.debug("claude-adapter",`[pty-probe] session=${this.sessionId??"?"} event=${r} win2s title=${this.ptyProbe.title} spinner=${this.ptyProbe.spinner} meaningful=${this.ptyProbe.meaningful} meaningfulGapMs=${a}`),this.ptyProbe.title=0,this.ptyProbe.spinner=0,this.ptyProbe.meaningful=0,this.ptyProbe.lastLogAt=t}}terminalPermissionPromptTail="";terminalPermissionPromptVisible=!1;terminalPermissionKeyTimer=null;trackTerminalPermissionPrompt(e){const t=e.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g," ").replace(/[\x00-\x1f\x7f-\x9f]/g," ").trim();if(t){if(hi.test(t)){this.terminalPermissionPromptTail=`${this.terminalPermissionPromptTail} ${t}`.slice(-di),ui.test(this.terminalPermissionPromptTail)&&(this.terminalPermissionPromptVisible=!0);return}t.length<=3||fi.test(t)||(this.terminalPermissionPromptTail="",this.terminalPermissionPromptVisible=!1)}}sendTerminalKey(e){const t=e==="enter"?"\r":"\x1B";try{if(this.claudePty)return this.claudePty.write(t),!0;if(this.expectRunDir)return this.writeExpectFifoLine(this.expectRunDir,`${de}${e}`),!0}catch(i){o.warn("claude-adapter",`Failed to send terminal key ${e}: ${i instanceof Error?i.message:i}`)}return!1}scheduleTerminalPermissionKey(e,t){this.terminalPermissionKeyTimer&&clearTimeout(this.terminalPermissionKeyTimer),this.terminalPermissionKeyTimer=setTimeout(()=>{if(this.terminalPermissionKeyTimer=null,this.stopped||!this.terminalPermissionPromptVisible)return;const i=t==="allow"?"enter":"esc";this.sendTerminalKey(i)&&(this.terminalPermissionPromptTail="",this.terminalPermissionPromptVisible=!1,o.info("claude-adapter",`Permission prompt still on terminal after ${t}, sent ${i} key: approvalId=${e}`))},ci),this.terminalPermissionKeyTimer.unref()}handleClaudeOutput(e){this.recordPtyProbe(e),this.trackTerminalPermissionPrompt(e),this.compacting&&this.resetCompactingActivityTimer();const t=e.trim();if(t){const a=t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202f\ufeff]/g,"").trim();a?a.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(a)?o.debug("claude-adapter",`[claude output] ${a}`):o.info("claude-adapter",`[claude output] ${a.slice(0,500)}`):o.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 a=i[1];this.sessionIdConflictDetected=!0;const l=[this.claudeProcess?.pid,this.claudeChildPid,this.claudePty?.pid].filter(c=>!!c&&c>0);he(a,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)&&(o.info("claude-adapter","Auto-accepting workspace trust dialog"),this.claudePty.write("1\r")),!this.startupDialogSeenAt&&!this.startupChannelListening&&/loadingdevelopmentchannels|usingthisforlocaldevelopment/.test(n)&&(this.startupDialogSeenAt=Date.now(),o.info("claude-adapter","Dev channels dialog rendered; restarting notify port wait window")),(/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,o.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){o.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()},oi))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),Bt.some(t=>t.test(e))&&(o.error("claude-adapter",`Auth failure: ${e}`),this.authFailureUntil=Date.now()+be,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
|
|
9
9
|
|
|
10
|
-
Error: Claude authentication failed -- please re-login`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude authentication failed -- please re-login"),this.clearActiveEvent())),
|
|
10
|
+
Error: Claude authentication failed -- please re-login`,1,!1),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude authentication failed -- please re-login"),this.clearActiveEvent())),Ut.some(t=>t.test(e))&&(o.error("claude-adapter",`Usage limit: ${e}`),this.usageLimitUntil=Date.now()+be,this.activeEvent&&(this.bridgeCallbacks.sendStreamChunk(this.activeEvent.eventId,this.activeEvent.sessionId,`
|
|
11
11
|
|
|
12
|
-
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())),
|
|
13
|
-
`}let a="";try{a=await A(s,"utf8")}catch{}a!==r&&(await O(s,r,"utf8"),o.info("claude-adapter",`Wrote Claude plugin manifest: ${s}`));const
|
|
14
|
-
`,p=f(t,"hooks"),m=f(p,"hooks.json");await M(p,{recursive:!0});let v="";try{v=await A(m,"utf8")}catch{}v!==h&&(await O(m,h,"utf8"),o.info("claude-adapter",`Wrote Claude hooks config: ${m}`));const E=f(t,"skills"),N=
|
|
15
|
-
`;"write"in t?t.write(i):t.write(i,()=>{}),o.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||(o.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||(o.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 a=this.startupChannelListeningAt||Date.now(),
|
|
12
|
+
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())),Ht.test(e)&&this.activeEvent&&(o.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=f(_().dataDir,"claude-plugin"),i=f(t,".claude-plugin"),s=f(i,"plugin.json"),n=f(e,".claude-plugin","plugin.json");await M(i,{recursive:!0});let r="";try{r=await A(n,"utf8")}catch{r=`${JSON.stringify({name:"grix-connector",version:"0.1.0",description:"Claude Code channel plugin for grix-connector.",license:"Apache-2.0"},null,2)}
|
|
13
|
+
`}let a="";try{a=await A(s,"utf8")}catch{}a!==r&&(await O(s,r,"utf8"),o.info("claude-adapter",`Wrote Claude plugin manifest: ${s}`));const l=f(e,"dist","scripts"),c=w=>`"${String(w).replace(/"/g,'\\"')}"`,u=w=>`${c(process.execPath)} ${c(f(l,w))}`,h=`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],Elicitation:[{hooks:[{type:"command",command:u("elicitation-hook.js"),timeout:630}]}],ElicitationResult:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],UserPromptSubmit:[{hooks:[{type:"command",command:u("user-prompt-submit-hook.js")}]}],PreToolUse:[{matcher:"ExitPlanMode",hooks:[{type:"command",command:u("approve-plan-hook.js")}]},{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PostToolUse:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PostToolUseFailure:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PermissionRequest:[{matcher:"",hooks:[{type:"command",command:u("permission-hook.js"),timeout:330}]}],Notification:[{matcher:"idle_prompt",hooks:[{type:"command",command:u("notification-hook.js")}]}],PermissionDenied:[{matcher:"",hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],Stop:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],StopFailure:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PreCompact:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],PostCompact:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}],ConfigChange:[{hooks:[{type:"command",command:u("lifecycle-hook.js")}]}]}},null,2)}
|
|
14
|
+
`,p=f(t,"hooks"),m=f(p,"hooks.json");await M(p,{recursive:!0});let v="";try{v=await A(m,"utf8")}catch{}v!==h&&(await O(m,h,"utf8"),o.info("claude-adapter",`Wrote Claude hooks config: ${m}`));const E=f(t,"skills"),N=Rt(E);return N.length>0&&o.info("claude-adapter",`Synced connector skills to plugin: [${N.join(", ")}]`),t}async ensureStdioMcpServer(){const e=this.resolveProjectRoot(),t=this.resolveStdioServerPath(e);if(this.ensureStdioServerArtifact(e,t),!C(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=f(e,"dist","mcp","mcp-bridge-server.js"),a=this.internalApi?.mcpBridgeWsUrl,l=C(r)&&a?[r,"--ws-url",a]:void 0;l||o.warn("claude-adapter",`APP MCP bridge skipped (path=${C(r)} ws=${!!a})`);const c=Ti(this.claudeCliSessionId);await bi(c,n,l),this.claudeMcpConfigPath=c,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=gt(import.meta.url);return mt(e,"..","..","..","..")}resolveStdioServerPath(e=this.resolveProjectRoot()){return f(e,"dist","mcp","stdio","server.js")}ensureStdioServerArtifact(e,t){if(C(t)||Je)return;Je=!0;const i=f(e,"node_modules","typescript","bin","tsc"),s=f(e,"tsconfig.json");if(!C(i)||!C(s))return;o.warn("claude-adapter",`MCP stdio server artifact missing, attempting build: ${t}`);const n=ie(process.execPath,[i,"-p",s],{cwd:e,env:process.env,encoding:"utf8",timeout:6e4,windowsHide:!0});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();let i=t+e,s=!1;const n=()=>{s||this.startupDialogSeenAt<=t||(s=!0,i=this.startupDialogSeenAt+e,o.info("claude-adapter",`Notify port wait extended: dialog appeared ${this.startupDialogSeenAt-t}ms after spawn, allowing ${e}ms from there`))};for(;n(),!(Date.now()>=i);)try{await new Promise((a,l)=>{const c=ae.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{c.destroy(),a()});c.on("error",l),setTimeout(()=>{c.destroy(),l(new Error("probe timeout"))},2e3)});return}catch{await new Promise(a=>setTimeout(a,500))}const r=Date.now()-t;throw new Error(`Notify port ${this.notifyPort} not ready within ${r}ms${s?" (window restarted at dev channels dialog)":""}`)}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>=V)return;await new Promise(r=>setTimeout(r,V-n));return}if(this.alive&&this.notifyPort>0&&Date.now()-t>i){o.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||(o.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}
|
|
15
|
+
`;"write"in t?t.write(i):t.write(i,()=>{}),o.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||(o.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||(o.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 a=this.startupChannelListeningAt||Date.now(),l=Date.now()-a;if(l>=V)return;await new Promise(c=>setTimeout(c,V-l));return}const n=Date.now()-t,r=this.claudePty?li:12e3;if(this.alive&&this.mcpServerReady&&n>r){o.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(a=>setTimeout(a,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=ae.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{this.notifySocket.write(i+`
|
|
16
16
|
`)}),this.notifySocket.on("error",s=>{o.error("claude-adapter",`Notify socket error: ${s.message}`),this.notifySocket=null}),this.notifySocket.on("close",()=>{this.notifySocket=null})):this.notifySocket.write(i+`
|
|
17
|
-
`)}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(){pi(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const t=this.claudeProcess,i=this.claudePty,s=this.claudeChildPid;if(e==="single-turn fallback isolation"&&(t&&this.singleTurnFallbackProcesses.add(t),i&&this.singleTurnFallbackPtys.add(i)),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 G(n.permissionRequestsDir);for(const[a]of this.pendingPermissions)r.resolveRequest(a,"deny").catch(()=>{});this.pendingPermissions.clear()}if(i)try{i.kill()}catch{}if(t?.pid&&H(t,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(t?.pid||i||s>0){const n=s,r=t,a=i;setTimeout(()=>{if(a)try{a.kill()}catch{}if(r?.pid&&H(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}o.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 o.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=z(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!t||t.stopReason!=="stop_sequence"||t.text.trim()!==Qt)return"none";if(this.malformedToolRetriedEventIds.has(e))return o.warn("claude-adapter",`Malformed tool-call retry exhausted for ${e}, sending clean fallback`),"exhausted";this.malformedToolRetriedEventIds.add(e);const i={...this.activeEvent.rawEvent,content:Yt};return o.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,s){if(e==="event_tool_call"){const n=String(t.tool_name??""),r=kt(n,{...t.arguments??{}}),a=this.getEventToolHandle();if(this.activeEvent){const l=this.activeEvent,u=String(r.event_id??"").trim();u===""?r.event_id=l.eventId:(n==="grix_reply"||n==="grix_complete")&&u!==l.eventId&&(o.warn("claude-adapter",`${n}: event_id mismatch, correcting: supplied=${u}, active=${l.eventId}`),r.event_id=l.eventId);const h=String(r.session_id??"").trim();if(n==="grix_reply"?(h&&h!==l.sessionId&&o.warn("claude-adapter",`${n}: session_id mismatch, correcting: supplied=${h}, active=${l.sessionId}`),r.session_id=l.sessionId):h===""&&(r.session_id=l.sessionId),n==="grix_reply"&&String(r.quoted_message_id??"").trim()===""){const p=String(l.rawEvent?.msg_id??"").trim();p&&(r.quoted_message_id=p)}n==="grix_reply"&&o.info("claude-adapter",`grix_reply quote: rawEvent.msg_id=${String(l.rawEvent?.msg_id??"")||"(none)"} -> quoted_message_id=${String(r.quoted_message_id??"")||"(none)"}`)}else if(this.lastClearedEvent&&n==="grix_reply"){const l=this.lastClearedEvent;Date.now()-l.ts<si&&(String(r.session_id??"").trim()===""&&(r.session_id=l.sessionId),r.event_id="",o.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${l.eventId} (cleared ${(Date.now()-l.ts)/1e3}s ago)`))}n==="grix_reply"&&String(r.event_id??"").trim()!==""&&this.completedEventIds.has(String(r.event_id??"").trim())&&(o.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(r.event_id??"").trim()}`),r.event_id="");const c=Tt(n,r);if(!c.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${c.error}`);if(a.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${a.status}`);if(Ct(n)){const l=this.activeEvent;l&&(l.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const u=Pt(a,n,r);if(u.isError)throw new Error(u.content[0]?.text??"event tool failed");if(i?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(n,r),JSON.parse(u.content[0]?.text??"null")}finally{if(l&&(l.toolCallInFlight=!1,l.pendingStopHook&&this.activeEvent===l)){const u=l.pendingStopHook;l.pendingStopHook=void 0,this.finalizeActiveEvent(u)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${n}`)}return this.bridgeCallbacks.agentInvoke(e,t,s)}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"){o.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){o.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))}}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 Pe(t)}catch{throw new Error(`pluginDir is not accessible: ${t}`)}if(!i.isDirectory())throw new Error(`pluginDir is not a directory: ${t}`);const s=f(t,".mcp.json");try{(await Pe(s)).isFile()&&(await we(s),o.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=f(I(),".claude.json");let i=e;try{i=Ee(e)}catch{}const s=i===e?[e]:[e,i];try{const n=await A(t,"utf8"),r=JSON.parse(n);r.projects||(r.projects={});let a=!1;for(const c of s)r.projects[c]?.hasTrustDialogAccepted!==!0&&(r.projects[c]||(r.projects[c]={}),r.projects[c].hasTrustDialogAccepted=!0,a=!0);if(!a)return;await O(t,JSON.stringify(r),"utf8"),o.info("claude-adapter",`Pre-trusted workspace: ${s.join(", ")}`)}catch(n){o.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${n}`)}}async ensureSkipDangerousPermissionPrompt(){const e=f(I(),".claude","settings.json");try{let t={};try{t=JSON.parse(await A(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await M(f(I(),".claude"),{recursive:!0}),await O(e,JSON.stringify(t,null,2),"utf8"),o.info("claude-adapter","Set skipDangerousModePermissionPrompt=true in user settings to skip BypassPermissions dialog")}catch(t){o.warn("claude-adapter",`Failed to set skipDangerousModePermissionPrompt: ${t}`)}}async isChannelGateClosed(){if(this.channelGateClosed)return!0;try{const e=await A(f(I(),".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=f(I(),".claude.json");try{let i;try{const r=await A(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),this.config.env?.ANTHROPIC_API_KEY===ke){const r=ke.slice(-20),a=i.customApiKeyResponses??{},c=a.approved??[],l=a.rejected??[];(!c.includes(r)||l.includes(r))&&(i.customApiKeyResponses={...a,approved:[...c.filter(u=>u!==r),r],rejected:l.filter(u=>u!==r)},s=!0)}if(!s)return;await O(t,JSON.stringify(i,null,2),"utf8"),o.info("claude-adapter",`Marked Claude onboarding complete: ${e}`)}catch(i){o.warn("claude-adapter",`Failed to mark Claude onboarding complete: ${i}`)}}async injectStatusLineSettings(e){try{const t=this.resolveProjectRoot(),i=f(t,"dist","scripts","status-line-forwarder.js"),s=f(e,".claude"),n=f(s,"settings.json");await M(s,{recursive:!0});let r={};try{r=JSON.parse(await A(n,"utf8"))}catch{}const c={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=c;const l=`${JSON.stringify(r,null,2)}
|
|
18
|
-
`;await O(n,l,"utf8"),o.info("claude-adapter",`Injected statusLine settings: ${n}`)}catch(t){o.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],a=f(I(),".claude.json");let c=null;try{const p=await A(a,"utf8");c=JSON.parse(p)?.mcpServers?.[F]??null}catch{}if(c&&String(c.type||"stdio").trim()==="stdio"&&String(c.command??"").trim()===n&&Array.isArray(c.args)&&c.args.length===r.length&&c.args.every((p,m)=>p===r[m]))return;o.info("claude-adapter",`Registering user-scoped MCP server: ${F} -> ${n} ${r.join(" ")}`);try{se(`${e} mcp remove -s user ${F}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe",windowsHide:!0})}catch{}const l=["mcp","add","--scope","user",F,"--",n,...r],u=process.platform==="win32"?'"':"'",h=se(`${e} ${l.map(p=>`${u}${p}${u}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe",windowsHide:!0});o.info("claude-adapter",`MCP server registered: ${h.trim()||"ok"}`)}resolveServerEntryPath(e){const t=f(e,"server","main.js");try{if(j(t))return t}catch{}const i=f(e,"dist","index.js");try{if(j(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(),this.clearRescueSettleTimer(),e&&(this.snapshotAuditBoundary(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=_(),t=this.claudeCliSessionId||this.sessionId;return f(e.dataDir,`hook-signals-${t}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=_().hookSignalsLogPath,i=new St(e,t);o.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new _t({hookSignalStore:i,onActivity:s=>this.onHookActivity(s),onTranscriptPath:s=>this.onHookTranscriptPath(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),onUserPromptSubmit:s=>this.onUserPromptSubmit(s)}),this.activityManager.start()}onHookTranscriptPath(e){this.claudeCliSessionId&&Ot(this.claudeCliSessionId,e)}onUserPromptSubmit(e){const t=this.activeEvent;!t||!e||e!==t.eventId||t.promptSubmittedAt!==void 0||(t.promptSubmittedAt=Date.now(),o.info("claude-adapter",`Prompt submitted for ${t.eventId}${t.channelPushedAt?` (${t.promptSubmittedAt-t.channelPushedAt}ms after push)`:""}`))}isStopBeforePromptSubmit(e){return e.channelPushedAt===void 0||e.promptSubmittedAt!==void 0?!1:Date.now()-e.channelPushedAt<ri}onClaudeTurnEnd(e){if(o.info("claude-adapter",`Hook activity: ${e}`),!this.activeEvent){this.stopHookBarrierSessionId&&(o.info("claude-adapter",`Late Stop hook after end_turn finalize \u2014 releasing barrier (session=${this.stopHookBarrierSessionId})`),this.clearStopHookBarrier());return}if(this.activeEvent.toolCallInFlight){o.info("claude-adapter",`Stop hook deferred: toolCallInFlight for ${this.activeEvent.eventId}`),this.activeEvent.pendingStopHook=e;return}if(!this.isEventAnswered()&&this.isStopBeforePromptSubmit(this.activeEvent)){o.warn("claude-adapter",`${e} hook ignored for ${this.activeEvent.eventId}: arrived before this event's prompt was submitted (${Date.now()-(this.activeEvent.channelPushedAt??0)}ms after push) \u2014 startup/resume turn tail`);return}if(this.activeEvent.singleTurnFallback&&!this.isEventAnswered()){o.info("claude-adapter",`${e} hook ignored for ${this.activeEvent.eventId}: single-turn fallback owns finalization`);return}this.finalizeActiveEvent(e)}forwardIncrementalText(e){const t=this.activeEvent;if(!t||t.eventId!==e||t.replied||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const i=t.textForwardOffset??t.jsonlBaseOffset??0,{segments:s,newOffset:n,apiError:r}=Dt(this.claudeCliSessionId,this.claudeSessionCwd,i);if(t.textForwardOffset=n,r&&!t.apiFormatError&&(t.apiFormatError=!0,o.error("claude-adapter",`API error entry detected in JSONL for ${e} (isApiErrorMessage), not forwarding its text`)),s.length===0)return;const a=t.sessionId;for(const c of s){const l=c.trim();if(!l)continue;const u=`txt_${e}_${this.textForwardSeq++}`;this.bridgeCallbacks.sendStreamChunk(e,a,l,1,!1,u),this.bridgeCallbacks.sendStreamChunk(e,a,"",2,!0,u),t.textForwarded||(t.textForwarded=!0,t.repliedAt===void 0&&(t.repliedAt=Date.now()),this.startPostReplyDeadline(e),this.startPostReplyJsonlWatcher(e))}this.markActiveEventActivity(e,a),o.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 o.info("claude-adapter",`Rescue skipped: no claudeCliSessionId or claudeSessionCwd for ${e}`),"none";const i=this.activeEvent?.jsonlBaseOffset,s=z(this.claudeCliSessionId,this.claudeSessionCwd,i);if(s?.apiError)return o.error("claude-adapter",`Rescue aborted for ${e}: turn ended with a CLI API error entry (isApiErrorMessage)`),this.activeEvent?.eventId===e&&(this.activeEvent.apiFormatError=!0),"none";if(s?.metaOnly)return i===void 0?(o.warn("claude-adapter",`Rescue for ${e}: only CLI meta text found but jsonlBaseOffset is unset \u2014 cannot attribute it to this turn, treating as unanswered`),"none"):(o.info("claude-adapter",`Rescue skipped for ${e}: this turn wrote only CLI meta text, finalizing silently`),"silent");const n=s?.text||null;if(!n)return o.info("claude-adapter",`Rescue failed: no assistant text found in JSONL for ${e}`),"none";const r=`rescue_${e}_${Date.now()}`;return this.bridgeCallbacks.sendStreamChunk(e,t,n,1,!1,r),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,r),o.info("claude-adapter",`Rescue succeeded for event ${e}: sent ${n.length} chars from JSONL`),"sent"}warnUnsentFinalIfAny(e){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=this.activeEvent?.jsonlBaseOffset,i=z(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))||o.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 s=z(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(s?.apiError)o.warn("claude-adapter",`Stop hook for ${t}: turn ended with a CLI API error entry, finalizing without waiting for end_turn`);else if(!s||s.stopReason!=="end_turn")if(Ae(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset))o.info("claude-adapter",`Stop hook: terminal tool_result detected for ${t} (grix_reply as final action, no end_turn expected), finalizing as responded`);else{o.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}}if(this.flushForwardedTail(t),!this.activeEvent.apiFormatError){if(this.activeEvent.replied||this.activeEvent.textForwarded){this.activeEvent.replied&&!this.activeEvent.textForwarded&&this.warnUnsentFinalIfAny(t),this.finishTurnEnd(t,i,e,!0,`replied=${this.activeEvent.replied}, textForwarded=${this.activeEvent.textForwarded}`);return}if(this.activeEvent.rescueSettleInFlight){o.info("claude-adapter",`${e} ignored for ${t}: rescue retry already in flight`);return}const s=this.recoverMalformedToolCall(t);if(s==="retried")return;if(s==="exhausted"){this.bridgeCallbacks.sendStreamChunk(t,i,Vt,1,!1,`mtc_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`mtc_${t}`),this.finishTurnEnd(t,i,e,!0,"malformed-toolcall-fallback");return}const n=this.attemptRescueFromJsonl(t,i);if(n!=="none"){this.finishTurnEnd(t,i,e,!0,n==="silent"?"no-response-requested":"rescued");return}if(this.activeEvent?.apiFormatError){this.finishTurnEnd(t,i,e,!1,"apiFormatError (detected during rescue)");return}this.beginRescueSettleWait(t,i,e);return}this.finishTurnEnd(t,i,e,!1,"apiFormatError")}beginRescueSettleWait(e,t,i){if(!this.activeEvent||this.activeEvent.eventId!==e)return;this.activeEvent.rescueSettleInFlight=!0;const s=Math.max(1,Math.ceil(Be/je));let n=0;const r=()=>{if(this.rescueSettleTimer=null,this.stopped)return;const a=this.activeEvent;if(!a||a.eventId!==e)return;if(n++,a.replied||a.textForwarded){this.finishTurnEnd(e,t,i,!0,`replied=${a.replied}, textForwarded=${a.textForwarded} (settled during flush wait)`);return}if(a.apiFormatError){this.finishTurnEnd(e,t,i,!1,"apiFormatError (settled during flush wait)");return}const c=this.attemptRescueFromJsonl(e,t);if(c!=="none"){this.finishTurnEnd(e,t,i,!0,c==="silent"?"no-response-requested-after-settle":"rescued-after-settle");return}if(a.apiFormatError){this.finishTurnEnd(e,t,i,!1,"apiFormatError (detected during rescue after settle)");return}if(i==="Stop"&&this.isToolOnlyTurnConcluded(a)){this.finishTurnEnd(e,t,i,!0,`tool-only-turn (toolCalls=${a.toolActivityCount??0})`);return}if(n>=s){this.finishTurnEnd(e,t,i,!1,`rescue-failed-after-settle (waited ~${Math.round(Be/1e3)}s, no transcript output for this turn)`);return}this.markActiveEventActivity(e,t),this.rescueSettleTimer=setTimeout(r,je)};this.rescueSettleTimer=setTimeout(r,zt)}isToolOnlyTurnConcluded(e){return!e.toolActivityCount||e.jsonlBaseOffset===void 0||!this.claudeCliSessionId||!this.claudeSessionCwd?!1:oe(this.claudeCliSessionId,this.claudeSessionCwd,e.jsonlBaseOffset).lastStopReason==="end_turn"}clearRescueSettleTimer(){this.rescueSettleTimer&&(clearTimeout(this.rescueSettleTimer),this.rescueSettleTimer=null)}finishTurnEnd(e,t,i,s,n){if(s)o.info("claude-adapter",`Stop hook received, finalizing event ${e} as responded (${n})`),this.bridgeCallbacks.sendEventResult(e,"responded");else{o.warn("claude-adapter",`Active event not confirmed when ${i}: ${e} (${n}), sending failed result`);const r=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(e,t,r,1,!1,`err_${e}`),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,`err_${e}`),this.bridgeCallbacks.sendEventResult(e,"failed",r,"agent_stop_failure")}this.clearActiveEvent(),this.armStopHookBarrier(t)}onHookActivity(e){const t=this.activeEvent?.sessionId;if(o.info("claude-adapter",`Hook activity: tool=${e?.tool_name??"(clear)"} session=${t??"(none)"}`),e&&this.activeEvent&&(this.activeEvent.toolActivityCount=(this.activeEvent.toolActivityCount??0)+1,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(e),!!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??"",a=n?At(n):!1;if(n==="ExitPlanMode"&&s==="PreToolUse")o.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)a||this.bridgeCallbacks.sendToolUse(i.eventId,i.sessionId,n,r);else if((s==="PostToolUse"||s==="PostToolUseFailure")&&n){if(n==="AskUserQuestion")return;!a&&s==="PostToolUseFailure"&&this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,"(failed)")}}}else this.startComposing(t)}handlePermissionHookEvent(e,t){if(!this.activeEvent){o.warn("claude-adapter","PermissionRequest without active event, ignoring");return}if(e==="AskUserQuestion"){o.info("claude-adapter","Skip permission card for AskUserQuestion; handled by agent_question card flow");return}const i=this.activeEvent,s=_();new G(s.permissionRequestsDir).listPending().then(r=>{const a=r.length>0?r[r.length-1]:null;if(!a){o.warn("claude-adapter","No pending permission request found in store");return}const c=a.request_id;this.pendingPermissions.set(c,{eventId:i.eventId,sessionId:i.sessionId});const l=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const p=Nt(l);p&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,p)}const u=e==="ExitPlanMode"?"":l.slice(0,100),h=u?`${e}: ${u}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:c,toolName:e,toolTitle:h}),o.info("claude-adapter",`Sent permission card: approvalId=${c} tool=${e}`)}).catch(r=>{o.warn("claude-adapter",`Failed to send permission card: ${r}`)})}async awaitPendingQuestionRequest(e,t){const i=Date.now()+Oe;for(;;){const s=await e.listPending(),n=Date.now(),r=[...s].reverse().filter(c=>String(c.session_id??"").trim()===this.claudeCliSessionId&&n-Number(c.created_at??0)<=$e),a=r.find(c=>String(c.event_id??"").trim()===t)??r.find(c=>String(c.event_id??"").trim()==="")??r[0]??null;if(a||Date.now()>=i)return a;await new Promise(c=>setTimeout(c,Ht))}}handleAskUserQuestion(e,t){const i=be(t);if(!i){o.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=_(),n=new Te(s.questionRequestsDir);this.awaitPendingQuestionRequest(n,e.eventId).then(async r=>{const a=r?.request_id??`fallback-${Date.now()}`;r||o.warn("claude-adapter",`No pending question waiter after ${Oe}ms; sending card with ${a}. Submitting it will report not_pending and fall back to a plain message.`),r&&String(r.event_id??"").trim()===""&&await n.updateRequest(r.request_id,{event_id:e.eventId}),this.pendingQuestion.set(a,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0;const l=(Number(r?.created_at??0)>0?Number(r?.created_at):Date.now())+Re;this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:a,mode:"form",questions:i,expires_at:l}),o.info("claude-adapter",`Sent agent_question card: request_id=${a} questions=${i.length}`)}).catch(()=>{o.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return be(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 a=String(r.header??r.question??`Question ${n+1}`),c=String(r.prompt??""),l=Array.isArray(r.options)?r.options:void 0,u=r.multiSelect===!0;i.push({header:a,prompt:c,...l&&l.length>0?{options:l}:{},...u?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-qt;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)return!1;const i=this.singleTurnFallbackChild,s=!!i&&i.exitCode===null&&!i.killed;if(!this.claudeProcess&&!this.claudePty&&!s)return!1;if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<He)return t.livenessExtendStartAt=Date.now(),!0;const n=oe(this.claudeCliSessionId,this.claudeSessionCwd,t.jsonlBaseOffset);if(n.freshMs===null||!(t.jsonlBaseOffset===void 0?n.freshMs<le:n.lastStopReason!==null?n.lastStopReason!=="end_turn":n.freshMs<le))return!1;if(n.freshMs<le)t.livenessExtendStartAt=Date.now();else{const a=t.livenessExtendStartAt??Date.now();if(t.livenessExtendStartAt===void 0&&(t.livenessExtendStartAt=a),Date.now()-a>Ue)return o.warn("claude-adapter",`Liveness extension budget exhausted for ${e} (no JSONL writes for ${Math.round((Date.now()-a)/6e4)}min), allowing close`),!1}return o.info("claude-adapter",`Liveness check: turn in progress for ${e} (lastStopReason=${n.lastStopReason??"none"}, freshMs=${n.freshMs}), extending`),!0}captureEventJsonlBaseOffset(e){this.captureAuditTranscriptStart(e);const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=B(this.claudeCliSessionId,this.claudeSessionCwd);if(t.auditTranscriptPath===i&&t.auditTranscriptStartOffset!==void 0){t.jsonlBaseOffset=t.auditTranscriptStartOffset;return}t.jsonlBaseOffset=C(i)?b(i).size:0}catch{t.jsonlBaseOffset=0}}captureSingleTurnFallbackJsonlBaseOffset(e){this.captureAuditTranscriptStart(e);const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=B(this.claudeCliSessionId,this.claudeSessionCwd);if(!C(i))return;if(t.auditTranscriptPath===i&&t.auditTranscriptStartOffset!==void 0){t.jsonlBaseOffset=t.auditTranscriptStartOffset;return}t.jsonlBaseOffset=b(i).size}catch{}}captureAuditTranscriptStart(e){const t=this.activeEvent;if(!t||t.eventId!==e||t.rawEvent?.audit?.enabled!==!0||t.auditTranscriptStartOffset!==void 0||!this.claudeCliSessionId)return;const i=this.claudeSessionCwd||process.cwd(),s=B(this.claudeCliSessionId,i);t.auditTranscriptPath=s,t.auditClaudeSessionId=this.claudeCliSessionId;try{t.auditTranscriptStartOffset=C(s)?b(s).size:0}catch{t.auditTranscriptStartOffset=0}const n=this.auditEvidencePaths;n&&(t.auditOtelLogsStartOffset=V(n.logsPath),t.auditOtelTracesStartOffset=V(n.tracesPath),n.rawProviderBody&&(t.auditRawBodyFilesBefore=Fe(n.rawBodyDir)))}snapshotAuditBoundary(e){if(e.rawEvent?.audit?.enabled!==!0)return;if(e.auditTranscriptStartOffset===void 0||!e.auditTranscriptPath||!e.auditClaudeSessionId){this.completedAuditBoundaries.set(e.eventId,Object.freeze({adapterType:this.type}));return}const t=this.claudeSessionCwd?B(e.auditClaudeSessionId,this.claudeSessionCwd):e.auditTranscriptPath,i=C(t)?t:e.auditTranscriptPath;let s=e.auditTranscriptStartOffset;try{s=C(i)?b(i).size:e.auditTranscriptStartOffset}catch{}const n=ht(i),r=this.auditEvidencePaths,a=new Set(e.auditRawBodyFilesBefore??[]),c=r?.rawProviderBody?Fe(r.rawBodyDir).filter(u=>!a.has(u)):[],l=Object.freeze({adapterType:this.type,providerSessionId:e.auditClaudeSessionId,transcriptPath:i,transcriptStartOffset:e.auditTranscriptStartOffset,transcriptEndOffset:s,...e.auditSingleTurnFallback?{singleTurnFallback:!0}:{},subagentRoot:f(n,e.auditClaudeSessionId,"subagents"),...r?{otelLogsPath:r.logsPath,otelLogsStartOffset:e.auditOtelLogsStartOffset??0,otelLogsEndOffset:V(r.logsPath),otelTracesPath:r.tracesPath,otelTracesStartOffset:e.auditOtelTracesStartOffset??0,otelTracesEndOffset:V(r.tracesPath),rawApiBodyDirectory:r.rawBodyDir,rawApiBodyPaths:c}:{}});this.completedAuditBoundaries.set(e.eventId,l)}noteSelfDrivenActivity(e){!this.selfDrivenActive&&this.lastClearedEvent&&Date.now()-this.lastClearedEvent.ts<ei||(this.selfDrivenLastSignalAt=Date.now(),e&&(this.selfDrivenLabel=ti(e)),!this.selfDrivenActive&&(this.selfDrivenActive=!0,o.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,o.info("claude-adapter",`Self-driven working state confirmed for session ${this.sessionId} (sustained >${qe/1e3}s), reporting`),this.emit("sessionActivity",this.sessionId??"",!0,this.selfDrivenLabel??void 0))},qe),this.selfDrivenReportTimer.unref(),this.selfDrivenSweepTimer=setInterval(()=>this.sweepSelfDriven(),Zt),this.selfDrivenSweepTimer.unref()))}sweepSelfDriven(){if(!this.selfDrivenActive)return;if(this.stopped||this.activeEvent){this.stopSelfDriven();return}const e=Date.now()-this.selfDrivenLastSignalAt;if(this.claudeCliSessionId&&this.claudeSessionCwd){const t=oe(this.claudeCliSessionId,this.claudeSessionCwd),i=t.lastStopReason!==null&&t.lastStopReason!=="end_turn",s=t.freshMs!==null&&t.freshMs<Ue,n=$t(this.claudeCliSessionId,this.claudeSessionCwd)>0;if((i||n)&&s){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0,this.selfDrivenLabel??void 0);return}}if(e<Xt){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0,this.selfDrivenLabel??void 0);return}o.info("claude-adapter",`Self-driven activity ended for session ${this.sessionId} (quiet for ${Math.round(e/1e3)}s, no pending work)`),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.selfDrivenLabel=null,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){o.info("claude-adapter",`Idle timeout skipped: toolCallInFlight for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.activeEvent?.awaitingUserQuestion){o.info("claude-adapter",`Idle timeout skipped: awaitingUserQuestion for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.pendingPermissions.size>0){o.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")}},De)}finalizeStuckActiveEvent(e,t){const i=this.isEventAnswered(),s=i?"responded":"failed",n=t==="idle"?De:Le,r=i?void 0:t==="idle"?`agent idle for ${n/1e3}s`:`agent exceeded max duration ${n/1e3}s`,a=i?void 0:t==="idle"?"agent_idle_timeout":"agent_hard_timeout";o.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,a),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")}},Le)}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=B(this.claudeCliSessionId,this.claudeSessionCwd);if(!this.activeEvent?.sessionId)return;if(!C(t)){o.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=b(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;try{this.activeEventPostReplyWatcher=dt(t,()=>{n&&clearTimeout(n),n=setTimeout(()=>this.finalizeIfEndTurn(e),300)}),this.activeEventPostReplyPoll=setInterval(()=>this.finalizeIfEndTurn(e),Kt),o.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(r){o.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=z(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(t?.apiError){this.activeEvent.apiFormatError=!0;return}if(t&&t.stopReason==="end_turn"){o.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&&Ae(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset,!0)){if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<He)return;o.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);o.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()},Wt)}beginCompaction(){this.compacting||(this.compacting=!0,o.info("claude-adapter","Compaction started; gating input"),this.emit("pauseIntake","compaction"),this.resetCompactingActivityTimer())}resetCompactingActivityTimer(){this.compactingTimer&&clearTimeout(this.compactingTimer),this.compactingTimer=setTimeout(()=>{o.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),o.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&&(o.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier())},ni)}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,o.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,`
|
|
17
|
+
`)}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(){mi(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const t=this.claudeProcess,i=this.claudePty,s=this.claudeChildPid;if(e==="single-turn fallback isolation"&&(t&&this.singleTurnFallbackProcesses.add(t),i&&this.singleTurnFallbackPtys.add(i)),e===De&&(t&&this.lostInjectionKilledProcesses.add(t),i&&this.lostInjectionKilledPtys.add(i)),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 G(n.permissionRequestsDir);for(const[a]of this.pendingPermissions)r.resolveRequest(a,"deny").catch(()=>{});this.pendingPermissions.clear()}if(i)try{i.kill()}catch{}if(t?.pid&&H(t,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(t?.pid||i||s>0){const n=s,r=t,a=i;setTimeout(()=>{if(a)try{a.kill()}catch{}if(r?.pid&&H(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}o.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 o.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=z(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(!t||t.stopReason!=="stop_sequence"||t.text.trim()!==Yt)return"none";if(this.malformedToolRetriedEventIds.has(e))return o.warn("claude-adapter",`Malformed tool-call retry exhausted for ${e}, sending clean fallback`),"exhausted";this.malformedToolRetriedEventIds.add(e);const i={...this.activeEvent.rawEvent,content:Kt};return o.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,s){if(e==="mcp_stdio_log")return o.info("claude-mcp-stdio",String(t.message??"")),{ok:!0};if(e==="event_tool_call"){const n=String(t.tool_name??""),r=It(n,{...t.arguments??{}}),a=this.getEventToolHandle();if(this.activeEvent){const c=this.activeEvent,u=String(r.event_id??"").trim();u===""?r.event_id=c.eventId:(n==="grix_reply"||n==="grix_complete")&&u!==c.eventId&&(o.warn("claude-adapter",`${n}: event_id mismatch, correcting: supplied=${u}, active=${c.eventId}`),r.event_id=c.eventId);const h=String(r.session_id??"").trim();if(n==="grix_reply"?(h&&h!==c.sessionId&&o.warn("claude-adapter",`${n}: session_id mismatch, correcting: supplied=${h}, active=${c.sessionId}`),r.session_id=c.sessionId):h===""&&(r.session_id=c.sessionId),n==="grix_reply"&&String(r.quoted_message_id??"").trim()===""){const p=String(c.rawEvent?.msg_id??"").trim();p&&(r.quoted_message_id=p)}n==="grix_reply"&&o.info("claude-adapter",`grix_reply quote: rawEvent.msg_id=${String(c.rawEvent?.msg_id??"")||"(none)"} -> quoted_message_id=${String(r.quoted_message_id??"")||"(none)"}`)}else if(this.lastClearedEvent&&n==="grix_reply"){const c=this.lastClearedEvent;Date.now()-c.ts<ni&&(String(r.session_id??"").trim()===""&&(r.session_id=c.sessionId),r.event_id="",o.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${c.eventId} (cleared ${(Date.now()-c.ts)/1e3}s ago)`))}n==="grix_reply"&&String(r.event_id??"").trim()!==""&&this.completedEventIds.has(String(r.event_id??"").trim())&&(o.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(r.event_id??"").trim()}`),r.event_id="");const l=At(n,r);if(!l.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${l.error}`);if(a.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${a.status}`);if(Tt(n)){const c=this.activeEvent;c&&(c.toolCallInFlight=!0);try{if(i?.aborted)throw new Error("invoke aborted by timeout");const u=Ct(a,n,r);if(u.isError)throw new Error(u.content[0]?.text??"event tool failed");if(i?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(n,r),JSON.parse(u.content[0]?.text??"null")}finally{if(c&&(c.toolCallInFlight=!1,c.pendingStopHook&&this.activeEvent===c)){const u=c.pendingStopHook;c.pendingStopHook=void 0,this.finalizeActiveEvent(u)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${n}`)}return this.bridgeCallbacks.agentInvoke(e,t,s)}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"){o.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){o.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))}}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 Pe(t)}catch{throw new Error(`pluginDir is not accessible: ${t}`)}if(!i.isDirectory())throw new Error(`pluginDir is not a directory: ${t}`);const s=f(t,".mcp.json");try{(await Pe(s)).isFile()&&(await we(s),o.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const t=f(b(),".claude.json");let i=e;try{i=Ee(e)}catch{}const s=i===e?[e]:[e,i];try{const n=await A(t,"utf8"),r=JSON.parse(n);r.projects||(r.projects={});let a=!1;for(const l of s)r.projects[l]?.hasTrustDialogAccepted!==!0&&(r.projects[l]||(r.projects[l]={}),r.projects[l].hasTrustDialogAccepted=!0,a=!0);if(!a)return;await O(t,JSON.stringify(r),"utf8"),o.info("claude-adapter",`Pre-trusted workspace: ${s.join(", ")}`)}catch(n){o.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${n}`)}}async ensureSkipDangerousPermissionPrompt(){const e=f(b(),".claude","settings.json");try{let t={};try{t=JSON.parse(await A(e,"utf8"))}catch{}if(t.skipDangerousModePermissionPrompt===!0)return;t.skipDangerousModePermissionPrompt=!0,await M(f(b(),".claude"),{recursive:!0}),await O(e,JSON.stringify(t,null,2),"utf8"),o.info("claude-adapter","Set skipDangerousModePermissionPrompt=true in user settings to skip BypassPermissions dialog")}catch(t){o.warn("claude-adapter",`Failed to set skipDangerousModePermissionPrompt: ${t}`)}}async isChannelGateClosed(){if(this.channelGateClosed)return!0;try{const e=await A(f(b(),".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=f(b(),".claude.json");try{let i;try{const r=await A(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),this.config.env?.ANTHROPIC_API_KEY===ke){const r=ke.slice(-20),a=i.customApiKeyResponses??{},l=a.approved??[],c=a.rejected??[];(!l.includes(r)||c.includes(r))&&(i.customApiKeyResponses={...a,approved:[...l.filter(u=>u!==r),r],rejected:c.filter(u=>u!==r)},s=!0)}if(!s)return;await O(t,JSON.stringify(i,null,2),"utf8"),o.info("claude-adapter",`Marked Claude onboarding complete: ${e}`)}catch(i){o.warn("claude-adapter",`Failed to mark Claude onboarding complete: ${i}`)}}async injectStatusLineSettings(e){try{const t=this.resolveProjectRoot(),i=f(t,"dist","scripts","status-line-forwarder.js"),s=f(e,".claude"),n=f(s,"settings.json");await M(s,{recursive:!0});let r={};try{r=JSON.parse(await A(n,"utf8"))}catch{}const l={type:"command",command:`node "${i.replace(/\\/g,"/")}"`,refreshInterval:10};r.statusLine=l;const c=`${JSON.stringify(r,null,2)}
|
|
18
|
+
`;await O(n,c,"utf8"),o.info("claude-adapter",`Injected statusLine settings: ${n}`)}catch(t){o.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],a=f(b(),".claude.json");let l=null;try{const p=await A(a,"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,m)=>p===r[m]))return;o.info("claude-adapter",`Registering user-scoped MCP server: ${F} -> ${n} ${r.join(" ")}`);try{se(`${e} mcp remove -s user ${F}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe",windowsHide:!0})}catch{}const c=["mcp","add","--scope","user",F,"--",n,...r],u=process.platform==="win32"?'"':"'",h=se(`${e} ${c.map(p=>`${u}${p}${u}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:i,stdio:"pipe",windowsHide:!0});o.info("claude-adapter",`MCP server registered: ${h.trim()||"ok"}`)}resolveServerEntryPath(e){const t=f(e,"server","main.js");try{if(j(t))return t}catch{}const i=f(e,"dist","index.js");try{if(j(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(),this.clearRescueSettleTimer(),e&&(this.snapshotAuditBoundary(e),this.sessionIdConflictRetriedEventIds.delete(e.eventId),this.malformedToolRetriedEventIds.delete(e.eventId),this.lostInjectionRetriedEventIds.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=_(),t=this.claudeCliSessionId||this.sessionId;return f(e.dataDir,`hook-signals-${t}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),t=_().hookSignalsLogPath,i=new wt(e,t);o.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new St({hookSignalStore:i,onActivity:s=>this.onHookActivity(s),onTranscriptPath:s=>this.onHookTranscriptPath(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),onUserPromptSubmit:s=>this.onUserPromptSubmit(s)}),this.activityManager.start()}onHookTranscriptPath(e){this.claudeCliSessionId&&Lt(this.claudeCliSessionId,e)}onUserPromptSubmit(e){const t=this.activeEvent;!t||!e||e!==t.eventId||t.promptSubmittedAt!==void 0||(t.promptSubmittedAt=Date.now(),o.info("claude-adapter",`Prompt submitted for ${t.eventId}${t.channelPushedAt?` (${t.promptSubmittedAt-t.channelPushedAt}ms after push)`:""}`))}isStopBeforePromptSubmit(e){return e.channelPushedAt===void 0||e.promptSubmittedAt!==void 0?!1:Date.now()-e.channelPushedAt<ai}onClaudeTurnEnd(e){if(o.info("claude-adapter",`Hook activity: ${e}`),!this.activeEvent){this.stopHookBarrierSessionId&&(o.info("claude-adapter",`Late Stop hook after end_turn finalize \u2014 releasing barrier (session=${this.stopHookBarrierSessionId})`),this.clearStopHookBarrier());return}if(this.activeEvent.toolCallInFlight){o.info("claude-adapter",`Stop hook deferred: toolCallInFlight for ${this.activeEvent.eventId}`),this.activeEvent.pendingStopHook=e;return}if(!this.isEventAnswered()&&this.isStopBeforePromptSubmit(this.activeEvent)){o.warn("claude-adapter",`${e} hook ignored for ${this.activeEvent.eventId}: arrived before this event's prompt was submitted (${Date.now()-(this.activeEvent.channelPushedAt??0)}ms after push) \u2014 startup/resume turn tail`);return}if(this.activeEvent.singleTurnFallback&&!this.isEventAnswered()){o.info("claude-adapter",`${e} hook ignored for ${this.activeEvent.eventId}: single-turn fallback owns finalization`);return}this.finalizeActiveEvent(e)}forwardIncrementalText(e){const t=this.activeEvent;if(!t||t.eventId!==e||t.replied||!this.claudeCliSessionId||!this.claudeSessionCwd)return;const i=t.textForwardOffset??t.jsonlBaseOffset??0,{segments:s,newOffset:n,apiError:r}=Dt(this.claudeCliSessionId,this.claudeSessionCwd,i);if(t.textForwardOffset=n,r&&!t.apiFormatError&&(t.apiFormatError=!0,o.error("claude-adapter",`API error entry detected in JSONL for ${e} (isApiErrorMessage), not forwarding its text`)),s.length===0)return;const a=t.sessionId;for(const l of s){const c=l.trim();if(!c)continue;const u=`txt_${e}_${this.textForwardSeq++}`;this.bridgeCallbacks.sendStreamChunk(e,a,c,1,!1,u),this.bridgeCallbacks.sendStreamChunk(e,a,"",2,!0,u),t.textForwarded||(t.textForwarded=!0,t.repliedAt===void 0&&(t.repliedAt=Date.now()),this.startPostReplyDeadline(e),this.startPostReplyJsonlWatcher(e))}this.markActiveEventActivity(e,a),o.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 o.info("claude-adapter",`Rescue skipped: no claudeCliSessionId or claudeSessionCwd for ${e}`),"none";const i=this.activeEvent?.jsonlBaseOffset,s=z(this.claudeCliSessionId,this.claudeSessionCwd,i);if(s?.apiError)return o.error("claude-adapter",`Rescue aborted for ${e}: turn ended with a CLI API error entry (isApiErrorMessage)`),this.activeEvent?.eventId===e&&(this.activeEvent.apiFormatError=!0),"none";if(s?.metaOnly)return i===void 0?(o.warn("claude-adapter",`Rescue for ${e}: only CLI meta text found but jsonlBaseOffset is unset \u2014 cannot attribute it to this turn, treating as unanswered`),"none"):(o.info("claude-adapter",`Rescue skipped for ${e}: this turn wrote only CLI meta text, finalizing silently`),"silent");const n=s?.text||null;if(!n)return o.info("claude-adapter",`Rescue failed: no assistant text found in JSONL for ${e}`),"none";const r=`rescue_${e}_${Date.now()}`;return this.bridgeCallbacks.sendStreamChunk(e,t,n,1,!1,r),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,r),o.info("claude-adapter",`Rescue succeeded for event ${e}: sent ${n.length} chars from JSONL`),"sent"}warnUnsentFinalIfAny(e){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return;const t=this.activeEvent?.jsonlBaseOffset,i=z(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))||o.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 s=z(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(s?.apiError)o.warn("claude-adapter",`Stop hook for ${t}: turn ended with a CLI API error entry, finalizing without waiting for end_turn`);else if(!s||s.stopReason!=="end_turn")if(Ae(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset))o.info("claude-adapter",`Stop hook: terminal tool_result detected for ${t} (grix_reply as final action, no end_turn expected), finalizing as responded`);else{o.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}}if(this.flushForwardedTail(t),!this.activeEvent.apiFormatError){if(this.activeEvent.replied||this.activeEvent.textForwarded){this.activeEvent.replied&&!this.activeEvent.textForwarded&&this.warnUnsentFinalIfAny(t),this.finishTurnEnd(t,i,e,!0,`replied=${this.activeEvent.replied}, textForwarded=${this.activeEvent.textForwarded}`);return}if(this.activeEvent.rescueSettleInFlight){o.info("claude-adapter",`${e} ignored for ${t}: rescue retry already in flight`);return}const s=this.recoverMalformedToolCall(t);if(s==="retried")return;if(s==="exhausted"){this.bridgeCallbacks.sendStreamChunk(t,i,Vt,1,!1,`mtc_${t}`),this.bridgeCallbacks.sendStreamChunk(t,i,"",2,!0,`mtc_${t}`),this.finishTurnEnd(t,i,e,!0,"malformed-toolcall-fallback");return}const n=this.attemptRescueFromJsonl(t,i);if(n!=="none"){this.finishTurnEnd(t,i,e,!0,n==="silent"?"no-response-requested":"rescued");return}if(this.activeEvent?.apiFormatError){this.finishTurnEnd(t,i,e,!1,"apiFormatError (detected during rescue)");return}this.beginRescueSettleWait(t,i,e);return}this.finishTurnEnd(t,i,e,!1,"apiFormatError")}beginRescueSettleWait(e,t,i){if(!this.activeEvent||this.activeEvent.eventId!==e)return;this.activeEvent.rescueSettleInFlight=!0;const s=Math.max(1,Math.ceil(Ue/Be));let n=0;const r=()=>{if(this.rescueSettleTimer=null,this.stopped)return;const a=this.activeEvent;if(!a||a.eventId!==e)return;if(n++,a.replied||a.textForwarded){this.finishTurnEnd(e,t,i,!0,`replied=${a.replied}, textForwarded=${a.textForwarded} (settled during flush wait)`);return}if(a.apiFormatError){this.finishTurnEnd(e,t,i,!1,"apiFormatError (settled during flush wait)");return}const l=this.attemptRescueFromJsonl(e,t);if(l!=="none"){this.finishTurnEnd(e,t,i,!0,l==="silent"?"no-response-requested-after-settle":"rescued-after-settle");return}if(a.apiFormatError){this.finishTurnEnd(e,t,i,!1,"apiFormatError (detected during rescue after settle)");return}if(i==="Stop"&&this.isToolOnlyTurnConcluded(a)){this.finishTurnEnd(e,t,i,!0,`tool-only-turn (toolCalls=${a.toolActivityCount??0})`);return}if(n>=s){this.finishTurnEnd(e,t,i,!1,`rescue-failed-after-settle (waited ~${Math.round(Ue/1e3)}s, no transcript output for this turn)`);return}this.markActiveEventActivity(e,t),this.rescueSettleTimer=setTimeout(r,Be)};this.rescueSettleTimer=setTimeout(r,Jt)}isToolOnlyTurnConcluded(e){return!e.toolActivityCount||e.jsonlBaseOffset===void 0||!this.claudeCliSessionId||!this.claudeSessionCwd?!1:oe(this.claudeCliSessionId,this.claudeSessionCwd,e.jsonlBaseOffset).lastStopReason==="end_turn"}clearRescueSettleTimer(){this.rescueSettleTimer&&(clearTimeout(this.rescueSettleTimer),this.rescueSettleTimer=null)}finishTurnEnd(e,t,i,s,n){if(s)o.info("claude-adapter",`Stop hook received, finalizing event ${e} as responded (${n})`),this.bridgeCallbacks.sendEventResult(e,"responded");else{o.warn("claude-adapter",`Active event not confirmed when ${i}: ${e} (${n}), sending failed result`);const r=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(e,t,r,1,!1,`err_${e}`),this.bridgeCallbacks.sendStreamChunk(e,t,"",2,!0,`err_${e}`),this.bridgeCallbacks.sendEventResult(e,"failed",r,"agent_stop_failure")}this.clearActiveEvent(),this.armStopHookBarrier(t)}onHookActivity(e){const t=this.activeEvent?.sessionId;if(o.info("claude-adapter",`Hook activity: tool=${e?.tool_name??"(clear)"} session=${t??"(none)"}`),e&&this.activeEvent&&(this.activeEvent.toolActivityCount=(this.activeEvent.toolActivityCount??0)+1,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(e),!!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??"",a=n?kt(n):!1;if(n==="ExitPlanMode"&&s==="PreToolUse")o.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)a||this.bridgeCallbacks.sendToolUse(i.eventId,i.sessionId,n,r);else if((s==="PostToolUse"||s==="PostToolUseFailure")&&n){if(n==="AskUserQuestion")return;!a&&s==="PostToolUseFailure"&&this.bridgeCallbacks.sendToolResult(i.eventId,i.sessionId,n,"(failed)")}}}else this.startComposing(t)}handlePermissionHookEvent(e,t){if(!this.activeEvent){o.warn("claude-adapter","PermissionRequest without active event, ignoring");return}if(e==="AskUserQuestion"){o.info("claude-adapter","Skip permission card for AskUserQuestion; handled by agent_question card flow");return}const i=this.activeEvent,s=_();new G(s.permissionRequestsDir).listPending().then(r=>{const a=r.length>0?r[r.length-1]:null;if(!a){o.warn("claude-adapter","No pending permission request found in store");return}const l=a.request_id;this.pendingPermissions.set(l,{eventId:i.eventId,sessionId:i.sessionId});const c=typeof t=="string"?t:"";if(e==="ExitPlanMode"){const p=jt(c);p&&this.bridgeCallbacks.sendReply(i.eventId,i.sessionId,p)}const u=e==="ExitPlanMode"?"":c.slice(0,100),h=u?`${e}: ${u}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,approvalId:l,toolName:e,toolTitle:h}),o.info("claude-adapter",`Sent permission card: approvalId=${l} tool=${e}`)}).catch(r=>{o.warn("claude-adapter",`Failed to send permission card: ${r}`)})}async awaitPendingQuestionRequest(e,t){const i=Date.now()+Oe;for(;;){const s=await e.listPending(),n=Date.now(),r=[...s].reverse().filter(l=>String(l.session_id??"").trim()===this.claudeCliSessionId&&n-Number(l.created_at??0)<=$e),a=r.find(l=>String(l.event_id??"").trim()===t)??r.find(l=>String(l.event_id??"").trim()==="")??r[0]??null;if(a||Date.now()>=i)return a;await new Promise(l=>setTimeout(l,qt))}}handleAskUserQuestion(e,t){const i=Ie(t);if(!i){o.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=_(),n=new Te(s.questionRequestsDir);this.awaitPendingQuestionRequest(n,e.eventId).then(async r=>{const a=r?.request_id??`fallback-${Date.now()}`;r||o.warn("claude-adapter",`No pending question waiter after ${Oe}ms; sending card with ${a}. Submitting it will report not_pending and fall back to a plain message.`),r&&String(r.event_id??"").trim()===""&&await n.updateRequest(r.request_id,{event_id:e.eventId}),this.pendingQuestion.set(a,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0;const c=(Number(r?.created_at??0)>0?Number(r?.created_at):Date.now())+Re;this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:a,mode:"form",questions:i,expires_at:c}),o.info("claude-adapter",`Sent agent_question card: request_id=${a} questions=${i.length}`)}).catch(()=>{o.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return Ie(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 a=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:a,prompt:l,...c&&c.length>0?{options:c}:{},...u?{multi_select:!0}:{}})}return i.length>0?{questions:i}:null}pruneCompletedEvents(){const e=Date.now()-Gt;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)return!1;const i=this.singleTurnFallbackChild,s=!!i&&i.exitCode===null&&!i.killed;if(!this.claudeProcess&&!this.claudePty&&!s)return!1;if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<qe)return t.livenessExtendStartAt=Date.now(),!0;const n=oe(this.claudeCliSessionId,this.claudeSessionCwd,t.jsonlBaseOffset);if(n.freshMs===null||!(t.jsonlBaseOffset===void 0?n.freshMs<le:n.lastStopReason!==null?n.lastStopReason!=="end_turn":n.freshMs<le))return!1;if(n.freshMs<le)t.livenessExtendStartAt=Date.now();else{const a=t.livenessExtendStartAt??Date.now();if(t.livenessExtendStartAt===void 0&&(t.livenessExtendStartAt=a),Date.now()-a>He)return o.warn("claude-adapter",`Liveness extension budget exhausted for ${e} (no JSONL writes for ${Math.round((Date.now()-a)/6e4)}min), allowing close`),!1}return o.info("claude-adapter",`Liveness check: turn in progress for ${e} (lastStopReason=${n.lastStopReason??"none"}, freshMs=${n.freshMs}), extending`),!0}captureEventJsonlBaseOffset(e){this.captureAuditTranscriptStart(e);const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=B(this.claudeCliSessionId,this.claudeSessionCwd);if(t.auditTranscriptPath===i&&t.auditTranscriptStartOffset!==void 0){t.jsonlBaseOffset=t.auditTranscriptStartOffset;return}t.jsonlBaseOffset=C(i)?I(i).size:0}catch{t.jsonlBaseOffset=0}}captureSingleTurnFallbackJsonlBaseOffset(e){this.captureAuditTranscriptStart(e);const t=this.activeEvent;if(!(!t||t.eventId!==e||t.jsonlBaseOffset!==void 0)&&!(!this.claudeCliSessionId||!this.claudeSessionCwd))try{const i=B(this.claudeCliSessionId,this.claudeSessionCwd);if(!C(i))return;if(t.auditTranscriptPath===i&&t.auditTranscriptStartOffset!==void 0){t.jsonlBaseOffset=t.auditTranscriptStartOffset;return}t.jsonlBaseOffset=I(i).size}catch{}}captureAuditTranscriptStart(e){const t=this.activeEvent;if(!t||t.eventId!==e||t.rawEvent?.audit?.enabled!==!0||t.auditTranscriptStartOffset!==void 0||!this.claudeCliSessionId)return;const i=this.claudeSessionCwd||process.cwd(),s=B(this.claudeCliSessionId,i);t.auditTranscriptPath=s,t.auditClaudeSessionId=this.claudeCliSessionId;try{t.auditTranscriptStartOffset=C(s)?I(s).size:0}catch{t.auditTranscriptStartOffset=0}const n=this.auditEvidencePaths;n&&(t.auditOtelLogsStartOffset=K(n.logsPath),t.auditOtelTracesStartOffset=K(n.tracesPath),n.rawProviderBody&&(t.auditRawBodyFilesBefore=Ne(n.rawBodyDir)))}snapshotAuditBoundary(e){if(e.rawEvent?.audit?.enabled!==!0)return;if(e.auditTranscriptStartOffset===void 0||!e.auditTranscriptPath||!e.auditClaudeSessionId){this.completedAuditBoundaries.set(e.eventId,Object.freeze({adapterType:this.type}));return}const t=this.claudeSessionCwd?B(e.auditClaudeSessionId,this.claudeSessionCwd):e.auditTranscriptPath,i=C(t)?t:e.auditTranscriptPath;let s=e.auditTranscriptStartOffset;try{s=C(i)?I(i).size:e.auditTranscriptStartOffset}catch{}const n=ft(i),r=this.auditEvidencePaths,a=new Set(e.auditRawBodyFilesBefore??[]),l=r?.rawProviderBody?Ne(r.rawBodyDir).filter(u=>!a.has(u)):[],c=Object.freeze({adapterType:this.type,providerSessionId:e.auditClaudeSessionId,transcriptPath:i,transcriptStartOffset:e.auditTranscriptStartOffset,transcriptEndOffset:s,...e.auditSingleTurnFallback?{singleTurnFallback:!0}:{},subagentRoot:f(n,e.auditClaudeSessionId,"subagents"),...r?{otelLogsPath:r.logsPath,otelLogsStartOffset:e.auditOtelLogsStartOffset??0,otelLogsEndOffset:K(r.logsPath),otelTracesPath:r.tracesPath,otelTracesStartOffset:e.auditOtelTracesStartOffset??0,otelTracesEndOffset:K(r.tracesPath),rawApiBodyDirectory:r.rawBodyDir,rawApiBodyPaths:l}:{}});this.completedAuditBoundaries.set(e.eventId,c)}noteSelfDrivenActivity(e){!this.selfDrivenActive&&this.lastClearedEvent&&Date.now()-this.lastClearedEvent.ts<ti||(this.selfDrivenLastSignalAt=Date.now(),e&&(this.selfDrivenLabel=ii(e)),!this.selfDrivenActive&&(this.selfDrivenActive=!0,o.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,o.info("claude-adapter",`Self-driven working state confirmed for session ${this.sessionId} (sustained >${Ge/1e3}s), reporting`),this.emit("sessionActivity",this.sessionId??"",!0,this.selfDrivenLabel??void 0))},Ge),this.selfDrivenReportTimer.unref(),this.selfDrivenSweepTimer=setInterval(()=>this.sweepSelfDriven(),ei),this.selfDrivenSweepTimer.unref()))}sweepSelfDriven(){if(!this.selfDrivenActive)return;if(this.stopped||this.activeEvent){this.stopSelfDriven();return}const e=Date.now()-this.selfDrivenLastSignalAt;if(this.claudeCliSessionId&&this.claudeSessionCwd){const t=oe(this.claudeCliSessionId,this.claudeSessionCwd),i=t.lastStopReason!==null&&t.lastStopReason!=="end_turn",s=t.freshMs!==null&&t.freshMs<He,n=Ot(this.claudeCliSessionId,this.claudeSessionCwd)>0;if((i||n)&&s){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0,this.selfDrivenLabel??void 0);return}}if(e<Zt){this.selfDrivenReported&&this.emit("sessionActivity",this.sessionId??"",!0,this.selfDrivenLabel??void 0);return}o.info("claude-adapter",`Self-driven activity ended for session ${this.sessionId} (quiet for ${Math.round(e/1e3)}s, no pending work)`),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.selfDrivenLabel=null,this.selfDrivenReported&&(this.selfDrivenReported=!1,this.emit("sessionActivity",this.sessionId??"",!1)))}tryRecoverLostInjection(e){const t=this.activeEvent;if(this.stopped||!t||t.eventId!==e||this.isEventAnswered(t)||t.channelPushedAt===void 0||t.promptSubmittedAt!==void 0||this.compacting||this.stopHookBarrierSessionId===t.sessionId||this.lostInjectionRetriedEventIds.has(e))return!1;this.lostInjectionRetriedEventIds.add(e);const i=t.rawEvent;return o.warn("claude-adapter",`Lost injection for ${e}: pushed ${Date.now()-t.channelPushedAt}ms ago but prompt never submitted \u2014 respawning Claude and re-pushing the event once`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.killClaudeProcess(De),this.deliverInboundEvent(i),!0}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(!this.stopped&&this.activeEvent?.eventId===e){if(this.activeEvent?.toolCallInFlight){o.info("claude-adapter",`Idle timeout skipped: toolCallInFlight for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.activeEvent?.awaitingUserQuestion){o.info("claude-adapter",`Idle timeout skipped: awaitingUserQuestion for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.pendingPermissions.size>0){o.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.tryRecoverLostInjection(e)||this.finalizeStuckActiveEvent(e,"idle")}},Le)}finalizeStuckActiveEvent(e,t){const i=this.isEventAnswered(),s=i?"responded":"failed",n=t==="idle"?Le:xe,r=i?void 0:t==="idle"?`agent idle for ${n/1e3}s`:`agent exceeded max duration ${n/1e3}s`,a=i?void 0:t==="idle"?"agent_idle_timeout":"agent_hard_timeout",l=this.lostInjectionRetriedEventIds.has(e)?", after lost-injection retry":"";o.error("claude-adapter",`Active event ${t} timeout (${n/1e3}s): ${e}, answered=${i}${l}, sending ${s}`),this.flushForwardedTail(e),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,s,r,a),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")}},xe)}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=B(this.claudeCliSessionId,this.claudeSessionCwd);if(!this.activeEvent?.sessionId)return;if(!C(t)){o.info("claude-adapter",`JSONL watcher skipped: file not yet available for ${e}`);return}const s=I(t).size;this.activeEvent&&(this.activeEvent.jsonlBaseOffset=s);let n=null;try{this.activeEventPostReplyWatcher=ut(t,()=>{n&&clearTimeout(n),n=setTimeout(()=>this.finalizeIfEndTurn(e),300)}),this.activeEventPostReplyPoll=setInterval(()=>this.finalizeIfEndTurn(e),Xt),o.info("claude-adapter",`JSONL watcher started for ${e} at ${t} (baseOffset=${s})`)}catch(r){o.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=z(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset);if(t?.apiError){this.activeEvent.apiFormatError=!0;return}if(t&&t.stopReason==="end_turn"){o.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&&Ae(this.claudeCliSessionId,this.claudeSessionCwd,this.activeEvent.jsonlBaseOffset,!0)){if(this.lastPtyOutputAt>0&&Date.now()-this.lastPtyOutputAt<qe)return;o.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);o.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()},Qt)}beginCompaction(){this.compacting||(this.compacting=!0,o.info("claude-adapter","Compaction started; gating input"),this.emit("pauseIntake","compaction"),this.resetCompactingActivityTimer())}resetCompactingActivityTimer(){this.compactingTimer&&clearTimeout(this.compactingTimer),this.compactingTimer=setTimeout(()=>{o.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),o.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&&(o.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier())},ri)}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,o.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,`
|
|
19
19
|
|
|
20
|
-
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
|
|
21
|
-
`)){const t=e.trim();if(!t)continue;let i;try{i=JSON.parse(t)}catch{continue}if(!(!i||typeof i!="object")){for(const s of
|
|
22
|
-
`),"utf8"),{expectPath:i,pidPath:s}}async function
|
|
23
|
-
${i.stderr??""}`;return s.trim()?{developmentChannels:e,maxTurns:s.includes("--max-turns")}:{...Q,developmentChannels:e}}catch(i){return o.warn("claude-adapter",`Claude capability probe failed, assuming modern CLI: ${i instanceof Error?i.message:String(i)}`),{...Q,developmentChannels:e}}}async function
|
|
24
|
-
`;let
|
|
20
|
+
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 Ei extends Ce{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){o.warn("claude-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}function S(d){if(d==null)return 0;const e=Number(d);return Number.isFinite(e)?e:0}function Qe(d){if(typeof d!="string"||!d.trim())return;let e;try{e=JSON.parse(d)}catch{return}if(!Array.isArray(e))return;const t=e.filter(i=>{if(!i||typeof i!="object")return!0;const s=String(i.content??"");return!Ye(s)});return JSON.stringify(t)}const yi=600;function Ye(d){const e=d.trim();return e?e.includes("\u4ECE\u7EBF\u4E0A\u8BB0\u5F55\u6062\u590D")||e.includes("\u672C\u4F1A\u8BDD\u7684\u672C\u5730\u5386\u53F2\u8BB0\u5F55\u5DF2\u4E22\u5931")||e.includes("\u672C\u5730\u5386\u53F2\u8BB0\u5F55\u5DF2\u4E22\u5931")||e.includes("\u65E0\u6CD5\u7EED\u63A5\u4E4B\u524D\u7684\u4E0A\u4E0B\u6587")?!0:e.length>yi?!1:/历史[^\n]{0,40}丢失/.test(e)&&/新建话题|恢复|怎么处理|如何继续|选哪个/.test(e):!1}function _i(d){const e=d.type;if(e!=="user"&&e!=="assistant")return[];const t=d.message,i=t&&typeof t=="object"?t.content:d.content;if(typeof i=="string")return[i];if(!Array.isArray(i))return[];const s=[];for(const n of i){if(!n||typeof n!="object")continue;const r=n;r.type==="text"&&typeof r.text=="string"&&s.push(r.text)}return s}function Si(d){for(const e of d.split(`
|
|
21
|
+
`)){const t=e.trim();if(!t)continue;let i;try{i=JSON.parse(t)}catch{continue}if(!(!i||typeof i!="object")){for(const s of _i(i))if(Ye(s))return!0}}return!1}function X(d){return String(d).replace(/\\/g,"\\\\").replace(/\{/g,"\\{").replace(/\}/g,"\\}")}const Ke={noFirst:"(?i)No,.{0,48}exit.*Yes,.{0,48}I.{0,48}trust(?:.*confirm)?",yesFirst:"(?i)Yes,.{0,48}(proceed|I.{0,48}trust).*No,.{0,48}exit(?:.*confirm)?"};async function wi(d,e,t){const i=f(d,"claude.expect"),s=f(d,"claude.pid"),n=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set cmd_fifo {${X(f(d,"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 {${X(e)}}${t.map(r=>` {${X(r)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${X(s)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","after 500",'send -- "\\r"',"expect {",` -re {${Ke.noFirst}} {`,' if {$startup_prompt_armed} { after 1000; send -- "\\033\\[B"; after 400; send -- "\\r"; after 300 }; exp_continue'," }",` -re {${Ke.yesFirst}} {`,' if {$startup_prompt_armed} { after 1000; send -- "\\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} { after 1000; send -- "\\r"; after 600; 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 eq "${de}enter"} {`,' send -- "\\r"',` } elseif {$__line eq "${de}esc"} {`,' send -- "\\033"',' } 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 O(s,"","utf8"),await O(i,n.join(`
|
|
22
|
+
`),"utf8"),{expectPath:i,pidPath:s}}async function Pi(d,e=50,t=100){for(let i=0;i<e;i++){try{const s=await A(d,"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 Ci(d,e){return new Promise((t,i)=>{let s=!1;const n=a=>{s||(s=!0,d.off("error",r),a())},r=a=>n(()=>i(a));d.once("error",r),Pi(e).then(a=>n(()=>t(a))).catch(a=>n(()=>i(a)))})}function Ve(d){return d.replace(/[/\\]/g,"-")}function Ti(d){const e=_();return f(e.dataDir,"claude-mcp-configs",`${Ve(d)}.json`)}function Ai(d){const e=_();return f(e.dataDir,"claude-system-prompts",`${Ve(d)}.txt`)}function ki(d){let e=Q.developmentChannels;const t=ie(d,["--dangerously-load-development-channels",`server:${F}`,"--version"],{encoding:"utf8",timeout:3e3,windowsHide:!0,env:Y(),...process.platform==="win32"?{shell:!0}:{}});t&&!t.error&&typeof t.status=="number"&&(e=t.status===0);try{const i=ie(d,["--help"],{encoding:"utf8",timeout:3e3,windowsHide:!0,env:Y(),...process.platform==="win32"?{shell:!0}:{}});if(!i)return{...Q,developmentChannels:e};const s=`${i.stdout??""}
|
|
23
|
+
${i.stderr??""}`;return s.trim()?{developmentChannels:e,maxTurns:s.includes("--max-turns")}:{...Q,developmentChannels:e}}catch(i){return o.warn("claude-adapter",`Claude capability probe failed, assuming modern CLI: ${i instanceof Error?i.message:String(i)}`),{...Q,developmentChannels:e}}}async function Ii(d){const e=f(d,"claude-extra-mcp-servers.json");let t;try{t=await A(e,"utf8")}catch{return{}}try{const s=JSON.parse(t)?.mcpServers;if(!s||typeof s!="object"||Array.isArray(s))return o.warn("claude-adapter",`Ignoring ${e}: missing "mcpServers" object`),{};const n={};for(const[r,a]of Object.entries(s)){const l=a;if(!l||typeof l!="object"||typeof l.command!="string"||!C(l.command)){o.warn("claude-adapter",`Skipping extra MCP server "${r}": command missing or not found`);continue}n[r]=a}return n}catch(i){return o.warn("claude-adapter",`Ignoring ${e}: invalid JSON (${i instanceof Error?i.message:String(i)})`),{}}}async function bi(d,e,t){const i=_();await M(f(i.dataDir,"claude-mcp-configs"),{recursive:!0});const n={...await Ii(i.configDir),[F]:{type:"stdio",command:process.execPath,args:e}};t&&(n["grix-app-bridge"]={type:"stdio",command:process.execPath,args:t});const a=`${JSON.stringify({mcpServers:n},null,2)}
|
|
24
|
+
`;let l="";try{l=await A(d,"utf8")}catch{}l!==a&&(await O(d,a,"utf8"),o.info("claude-adapter",`Wrote MCP config: ${d}`))}async function Ri(d,e){await M(f(_().dataDir,"claude-system-prompts"),{recursive:!0});const t=`${e}
|
|
25
25
|
`;let i="";try{i=await A(d,"utf8")}catch{}i!==t&&await O(d,t,"utf8")}function he(d,e){if(process.platform==="win32")return;let t=!1;try{const i=se(`ps ax -o pid,command | grep -E -- '--(session-id|resume) ${d}' | grep -v grep`,{encoding:"utf8",timeout:5e3,windowsHide:!0}).trim();if(i)for(const s of i.split(`
|
|
26
|
-
`)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){o.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${d}`);try{process.kill(n,"SIGTERM"),t=!0}catch{}}}}catch{}t||fe(d)}function fe(d){const e=f(
|
|
26
|
+
`)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){o.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${d}`);try{process.kill(n,"SIGTERM"),t=!0}catch{}}}}catch{}t||fe(d)}function fe(d){const e=f(b(),".claude"),t=[f(e,"session-env",d)];try{const i=f(e,"sessions");for(const s of re(i))if(s.endsWith(".json"))try{const n=f(i,s),r=JSON.parse(j(n,"utf8"));r&&r.sessionId===d&&t.push(n)}catch{}}catch{}for(const i of t)try{I(i),at(i,{recursive:!0,force:!0}),o.info("claude-adapter",`Removed stale session file: ${i}`)}catch{}}function cs(d,e){if(!e)return!1;const t=f(b(),".claude"),i=Z(e),s=f(t,"projects",i,`${d}.jsonl`);try{return I(s),!0}catch{return!1}}function Xe(d,e){if(!e)return!1;const t=f(b(),".claude","projects"),i=Z(e),s=f(t,i),n=f(s,`${d}.jsonl`),r=`${d}.jsonl`,a=[];let l;try{l=re(t)}catch{return!1}for(const h of l){const p=f(t,h,r);try{const m=I(p);m.isFile()&&m.size>0&&a.push({path:p,size:m.size,mtimeMs:m.mtimeMs,lines:-1})}catch{}}if(a.length===0)return!1;let c=a[0];if(a.length>1){for(const h of a)h.lines=pe(h.path);a.sort((h,p)=>p.lines-h.lines||p.size-h.size||p.mtimeMs-h.mtimeMs),c=a[0]}if(c.path===n)return!0;const u=a.find(h=>h.path===n);if(u){const h=u.lines>=0?u.lines:pe(u.path),p=c.lines>=0?c.lines:pe(c.path);if(h>=p)return!0}try{return _e(s,{recursive:!0}),ye(c.path,n),o.info("claude-adapter",`Reclaimed session ${d} history ${c.path} -> ${n} (${a.length} copies; cwd drift recovered via move)`),!0}catch(h){return o.warn("claude-adapter",`Reclaim session ${d} from ${c.path} failed: ${h.message}`),C(n)}}function Ze(d,e){if(!e)return!1;const t=f(b(),".claude","projects",Z(e),`${d}.jsonl`);let i="";try{i=j(t,"utf8")}catch{return!1}if(!Si(i))return!1;const s=`${t}.polluted-history-recovery-${Date.now()}`;try{return ye(t,s),fe(d),o.warn("claude-adapter",`Quarantined polluted Claude transcript ${t} -> ${s}; continuing without local resume`),!0}catch(n){return o.warn("claude-adapter",`Failed to quarantine polluted Claude transcript ${t}: ${n instanceof Error?n.message:String(n)}`),!1}}function pe(d){try{const e=j(d);let t=0;for(let i=0;i<e.length;i++)e[i]===10&&t++;return t}catch{return 0}}function et(d,e){if(!e)return;const t=f(b(),".claude"),i=Z(e),s=f(t,"projects",i,`${d}.jsonl`);let n;try{n=j(s)}catch{return}if(n.length===0)return;let r=-1;for(let c=n.length-1;c>=0;c--)if(n[c]===10){r=c;break}const a=r+1,l=n.subarray(a).toString("utf8").trim();if(l)try{JSON.parse(l)}catch{try{ot(s,a),o.warn("claude-adapter",`repairJsonl: stripped ${n.length-a}B invalid trailing data from ${d}`)}catch(c){o.error("claude-adapter",`repairJsonl: failed to truncate ${d}: ${c instanceof Error?c.message:c}`)}}}function $i(){return process.env.ComSpec||f(process.env.SystemRoot||"C:\\Windows","System32","cmd.exe")}function Z(d){const e=d.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=Ee(e)}catch{}return i.replace(/[^a-zA-Z0-9]/g,"-")}export{Ke as CLAUDE_TRUST_DIALOG_PATTERNS,ls as ClaudeAdapter,wi as createExpectScript,Z as deriveClaudeProjectDirName,Ye as isLostHistoryRecoveryNotice,Ii as loadClaudeExtraMcpServers,Ie as parseAskUserQuestionPayload,Wt as resolveExpectPath,$i as resolveWindowsShellCommand,Si as transcriptHasLostHistoryRecoveryNotice};
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bridgeVersion": "4.9.
|
|
4
|
-
"tarball": "grix-dsh-bridge-4.9.
|
|
5
|
-
"size":
|
|
6
|
-
"unpackedSize":
|
|
7
|
-
"shasum": "
|
|
8
|
-
"integrity": "sha512-
|
|
3
|
+
"bridgeVersion": "4.9.1",
|
|
4
|
+
"tarball": "grix-dsh-bridge-4.9.1.tgz",
|
|
5
|
+
"size": 23132,
|
|
6
|
+
"unpackedSize": 92698,
|
|
7
|
+
"shasum": "97579c42a23a1d2fcdcdccf473e33239409202e8",
|
|
8
|
+
"integrity": "sha512-qyX9J3iDELSbnnvkMXNDYkExFrRl7R2S2A0UtK7Yd4B7X3Tna0+T/xMZWlNiy+52h6NgNnqYnp3chpdy8aJm8Q=="
|
|
9
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import B from"node:http";import{randomBytes as M}from"node:crypto";import{log as h}from"../log/index.js";import{PHASE1_INVOKE_ACTIONS as C,TOOLS as O,toolCallToInvoke as P}from"./tools.js";import{WebSocketServer as R,WebSocket as $}from"ws";const y=1024*1024,U=32*1024*1024,N=50,k=1e3,f=75e3,H="2024-11-05",I=256,T=new Set([...C,"event_tool_call"]);class W{server=null;port=0;authToken;invokeHandler=null;statusLineHandler=null;otelHandler=null;activeInvokes=0;wss=null;bridgeWs=null;mcpBridgeUpHandler=null;downstreamOutbox=[];mcpBridgeHttpActive=!1;pendingBridgeResponses=new Map;codebaseNavigatorCwd=null;codebaseNavigatorFacade=null;constructor(){this.authToken=M(32).toString("hex")}setInvokeHandler(e){this.invokeHandler=e}setStatusLineHandler(e){this.statusLineHandler=e}setOtelHandler(e){this.otelHandler=e}async start(e=0){return new Promise((t,r)=>{this.server=B.createServer((n,i)=>this.handleRequest(n,i)),this.wss=new R({noServer:!0}),this.server.on("upgrade",(n,i,s)=>{const o=new URL(n.url??"/",`http://127.0.0.1:${this.port}`);if(o.pathname!=="/mcp-bridge"||o.searchParams.get("token")!==this.authToken){i.destroy();return}this.wss.handleUpgrade(n,i,s,a=>{if(this.bridgeWs=a,h.info("internal-api","mcp bridge connected"),this.downstreamOutbox.length>0){const c=this.downstreamOutbox;this.downstreamOutbox=[];for(const p of c)a.send(p);h.info("internal-api",`mcp bridge flushed ${c.length} buffered downstream frame(s)`)}a.on("message",c=>{let p;try{p=JSON.parse(c.toString("utf8"))}catch{return}this.mcpBridgeUpHandler?.(p)}),a.on("close",()=>{this.bridgeWs===a&&(this.bridgeWs=null),h.info("internal-api","mcp bridge disconnected")}),a.on("error",()=>{})})}),this.server.on("error",r),this.server.listen(e,"127.0.0.1",()=>{const n=this.server.address();typeof n=="object"&&n&&(this.port=n.port),this.server.removeListener("error",r),t(this.port)})})}get url(){return`http://127.0.0.1:${this.port}?token=${this.authToken}`}get baseUrl(){return`http://127.0.0.1:${this.port}`}get mcpBridgeWsUrl(){return`ws://127.0.0.1:${this.port}/mcp-bridge?token=${this.authToken}`}get mcpBridgeHttpUrl(){return this.mcpBridgeHttpActive=!0,`${this.baseUrl}/mcp/app-bridge`}get mcpToolsHttpUrl(){return`${this.baseUrl}/mcp/tools`}get codebaseNavigatorHttpUrl(){return`${this.baseUrl}/mcp/codebase-navigator`}async setCodebaseNavigatorCwd(e){if(this.codebaseNavigatorCwd===e&&this.codebaseNavigatorFacade)return;this.codebaseNavigatorCwd=e;const t=this.codebaseNavigatorFacade;this.codebaseNavigatorFacade=null,await t?.close();const i=(await import(new URL("../../kimi-plugins/grix-layer/scripts/http-facade.mjs",import.meta.url).href)).createCodebaseNavigatorHttpFacade;this.codebaseNavigatorFacade=i({cwd:e})}setMcpBridgeUpHandler(e){this.mcpBridgeUpHandler=e}sendMcpFrameToBridge(e){const t=_(e);if(t!==void 0){const n=E(t),i=this.pendingBridgeResponses.get(n);if(i){this.pendingBridgeResponses.delete(n),clearTimeout(i.timer),i.resolve(e);return}}const r=JSON.stringify(e);if(this.bridgeWs&&this.bridgeWs.readyState===$.OPEN){this.bridgeWs.send(r);return}if(this.mcpBridgeHttpActive){h.debug("internal-api","dropping unsolicited MCP bridge frame without an HTTP stream");return}this.downstreamOutbox.length>=I&&(this.downstreamOutbox.shift(),h.warn("internal-api","mcp downstream outbox full, dropping oldest frame")),this.downstreamOutbox.push(r)}get token(){return this.authToken}async stop(){const e=this.codebaseNavigatorFacade;if(this.codebaseNavigatorFacade=null,await e?.close(),!!this.server){for(const[t,r]of this.pendingBridgeResponses)clearTimeout(r.timer),r.reject(new Error("internal API stopped")),this.pendingBridgeResponses.delete(t);return new Promise(t=>{this.server.close(()=>{this.server=null,t()})})}}checkAuth(e){return e.headers.authorization===`Bearer ${this.authToken}`||new URL(e.url??"/",`http://127.0.0.1:${this.port}`).searchParams.get("token")===this.authToken}handleRequest(e,t){if(e.method!=="POST"){t.writeHead(405),t.end(JSON.stringify({error:"method not allowed"}));return}if(!this.checkAuth(e)){t.writeHead(401),t.end(JSON.stringify({ok:!1,error:"unauthorized"}));return}const r=new URL(e.url??"/",`http://127.0.0.1:${this.port}`);if(r.pathname==="/mcp/tools"){this.handleMcpTools(e,t);return}if(r.pathname==="/mcp/app-bridge"){this.handleMcpBridgeHttp(e,t);return}if(r.pathname==="/mcp/codebase-navigator"){this.handleCodebaseNavigatorMcp(e,t);return}if(r.pathname==="/api/status-line"){this.handleStatusLine(e,t);return}if(r.pathname==="/v1/logs"||r.pathname==="/v1/traces"){this.handleOtel(r.pathname==="/v1/logs"?"logs":"traces",e,t);return}if(!r.pathname.startsWith("/api/invoke")){t.writeHead(404),t.end(JSON.stringify({error:"not found"}));return}const n=[];let i=0;e.on("data",s=>{if(i+=s.length,i>y){t.writeHead(413),t.end(JSON.stringify({ok:!1,error:"request body too large"})),e.destroy();return}n.push(s)}),e.on("end",async()=>{let s;try{s=JSON.parse(Buffer.concat(n).toString("utf8"))}catch{t.writeHead(400),t.end(JSON.stringify({ok:!1,error:"invalid JSON"}));return}const o=s.action,a=s.params??{};if(!o||!this.invokeHandler){t.writeHead(400),t.end(JSON.stringify({ok:!1,error:"missing action or no handler"}));return}if(!T.has(o)){t.writeHead(400),t.end(JSON.stringify({ok:!1,error:`action '${o}' is not allowed`}));return}if(this.activeInvokes>=N){t.writeHead(429),t.end(JSON.stringify({ok:!1,error:"too many concurrent invokes"}));return}const c=s.timeout_ms!=null?Math.max(k,Math.min(s.timeout_ms,f)):f,p=Date.now();this.activeInvokes++;const m=new AbortController;try{const g=await Promise.race([this.invokeHandler(o,a,m.signal,c),new Promise((w,J)=>setTimeout(()=>{m.abort(),J(new Error("invoke timeout"))},c))]);t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,data:g})),h.info("internal-api",`invoke ok action=${o} tool=${a.tool_name??"-"} ms=${Date.now()-p}`)}catch(g){m.abort();const w=g instanceof Error?g.message:String(g);t.writeHead(500),t.end(JSON.stringify({ok:!1,error:w})),h.error("internal-api",`invoke error action=${o} tool=${a.tool_name??"-"} ms=${Date.now()-p} err=${w}`)}finally{this.activeInvokes--}})}async handleMcpTools(e,t){let r;try{r=b(await v(e))}catch(o){l(t,null,-32700,o instanceof Error?o.message:String(o),400);return}if(r.method==="notifications/initialized"){t.writeHead(202),t.end();return}const n=r.id??null;if(r.method==="initialize"){u(t,n,{protocolVersion:H,capabilities:{tools:{}},serverInfo:{name:"grix-connector-tools",version:"1.0.0"}});return}if(r.method==="ping"){u(t,n,{});return}if(r.method==="tools/list"){u(t,n,{tools:O.map(({name:o,description:a,inputSchema:c})=>({name:o,description:a,inputSchema:c}))});return}if(r.method!=="tools/call"){l(t,n,-32601,`Method not found: ${r.method}`);return}const i=String(r.params?.name??""),s=r.params?.arguments??{};if(!O.some(o=>o.name===i)){l(t,n,-32602,`Unknown tool: ${i}`);return}try{const o=P(i,s),a=await this.invokeMcpTool(o.action,o.params,o.timeoutMs);u(t,n,{content:[{type:"text",text:JSON.stringify(a,null,2)??"null"}]})}catch(o){const a=o instanceof Error?o.message:String(o);u(t,n,{content:[{type:"text",text:JSON.stringify({error:a})}],isError:!0})}}async handleMcpBridgeHttp(e,t){let r;try{r=b(await v(e))}catch(i){l(t,null,-32700,i instanceof Error?i.message:String(i),400);return}if(!this.mcpBridgeUpHandler){l(t,r.id??null,-32603,"MCP bridge is unavailable",503);return}if(r.id===void 0){this.mcpBridgeUpHandler(r),t.writeHead(202),t.end();return}const n=E(r.id);if(this.pendingBridgeResponses.has(n)){l(t,r.id,-32600,"Duplicate in-flight MCP request id",409);return}try{const i=await new Promise((s,o)=>{const a=setTimeout(()=>{this.pendingBridgeResponses.delete(n),o(new Error("MCP bridge response timeout"))},f);this.pendingBridgeResponses.set(n,{resolve:s,reject:o,timer:a});try{this.mcpBridgeUpHandler(r)}catch(c){clearTimeout(a),this.pendingBridgeResponses.delete(n),o(c instanceof Error?c:new Error(String(c)))}});S(t,200,i)}catch(i){l(t,r.id,-32603,i instanceof Error?i.message:String(i),504)}}async handleCodebaseNavigatorMcp(e,t){let r;try{r=b(await v(e))}catch(a){l(t,null,-32700,a instanceof Error?a.message:String(a),400);return}if(r.method==="notifications/initialized"){t.writeHead(202),t.end();return}const n=r.id??null;if(r.method==="initialize"){u(t,n,{protocolVersion:H,capabilities:{tools:{}},serverInfo:{name:"codebase-navigator",version:"0.1.0"}});return}if(r.method==="ping"){u(t,n,{});return}let i;try{i=await this.getCodebaseNavigatorFacade()}catch(a){l(t,n,-32603,a instanceof Error?a.message:String(a),503);return}if(r.method==="tools/list"){u(t,n,{tools:i.listTools()});return}if(r.method!=="tools/call"){l(t,n,-32601,`Method not found: ${r.method}`);return}const s=String(r.params?.name??""),o=r.params?.arguments??{};if(!i.listTools().some(a=>a.name===s)){l(t,n,-32602,`Unknown tool: ${s}`);return}try{u(t,n,await i.callTool(s,o))}catch(a){u(t,n,{content:[{type:"text",text:JSON.stringify({error:a instanceof Error?a.message:String(a)})}],isError:!0})}}async getCodebaseNavigatorFacade(){if(this.codebaseNavigatorFacade)return this.codebaseNavigatorFacade;if(!this.codebaseNavigatorCwd)throw new Error("codebase navigator cwd is not configured");if(await this.setCodebaseNavigatorCwd(this.codebaseNavigatorCwd),!this.codebaseNavigatorFacade)throw new Error("codebase navigator failed to initialize");return this.codebaseNavigatorFacade}async invokeMcpTool(e,t,r){if(!this.invokeHandler)throw new Error("invoke handler is unavailable");if(!T.has(e))throw new Error(`action '${e}' is not allowed`);if(this.activeInvokes>=N)throw new Error("too many concurrent invokes");const n=r!=null?Math.max(k,Math.min(r,f)):f,i=new AbortController;let s;this.activeInvokes++;try{return await Promise.race([this.invokeHandler(e,t,i.signal,n),new Promise((o,a)=>{s=setTimeout(()=>{i.abort(),a(new Error("invoke timeout"))},n)})])}finally{s&&clearTimeout(s),this.activeInvokes--}}handleStatusLine(e,t){const r=[];let n=0;e.on("data",i=>{if(n+=i.length,n>y){t.writeHead(413),t.end(JSON.stringify({ok:!1,error:"request body too large"})),e.destroy();return}r.push(i)}),e.on("end",async()=>{let i;try{i=JSON.parse(Buffer.concat(r).toString("utf8"))}catch{t.writeHead(400),t.end(JSON.stringify({ok:!1,error:"invalid JSON"}));return}if(!this.statusLineHandler){t.writeHead(200),t.end(JSON.stringify({ok:!0}));return}try{await this.statusLineHandler(i),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0}))}catch(s){console.error("[internal-api] statusLine handler error:",s instanceof Error?s.message:s),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0}))}})}handleOtel(e,t,r){const n=[];let i=0,s=!1;t.on("data",o=>{if(!s){if(i+=o.length,i>U){s=!0,r.writeHead(413,{"content-type":"application/json"}),r.end(JSON.stringify({error:"OTLP request body too large"}));return}n.push(o)}}),t.on("end",async()=>{if(s)return;const o=Buffer.concat(n).toString("utf8");let a;try{const c=JSON.parse(o);if(!c||typeof c!="object"||Array.isArray(c))throw new Error("OTLP body is not an object");a=c}catch{r.writeHead(400,{"content-type":"application/json"}),r.end(JSON.stringify({error:"invalid OTLP JSON"}));return}try{await this.otelHandler?.(e,a,o)}catch(c){h.warn("internal-api",`OTLP ${e} evidence persistence failed: ${c instanceof Error?c.message:String(c)}`)}r.writeHead(200,{"content-type":"application/json"}),r.end("{}")})}}function v(d){return new Promise((e,t)=>{const r=[];let n=0,i=!1;d.on("data",s=>{if(!i){if(n+=s.length,n>y){i=!0,t(new Error("request body too large")),d.resume();return}r.push(s)}}),d.on("end",()=>{if(!i){i=!0;try{e(JSON.parse(Buffer.concat(r).toString("utf8")))}catch{t(new Error("invalid JSON"))}}}),d.on("error",s=>{i||(i=!0,t(s))})})}function b(d){if(!d||typeof d!="object"||Array.isArray(d))throw new Error("invalid JSON-RPC request");const e=d;if(e.jsonrpc!=="2.0"||typeof e.method!="string")throw new Error("invalid JSON-RPC request");const t=e.id;if(t!=null&&typeof t!="string"&&typeof t!="number")throw new Error("invalid JSON-RPC id");return d}function _(d){if(!d||typeof d!="object"||Array.isArray(d)||!Object.prototype.hasOwnProperty.call(d,"id"))return;const e=d.id;if(e===null||typeof e=="string"||typeof e=="number")return e}function E(d){return`${typeof d}:${String(d)}`}function S(d,e,t){d.writeHead(e,{"content-type":"application/json"}),d.end(JSON.stringify(t))}function u(d,e,t){S(d,200,{jsonrpc:"2.0",id:e,result:t})}function l(d,e,t,r,n=200){S(d,n,{jsonrpc:"2.0",id:e,error:{code:t,message:r}})}export{W as InternalApiServer};
|
|
1
|
+
import B from"node:http";import{randomBytes as M}from"node:crypto";import{log as h}from"../log/index.js";import{PHASE1_INVOKE_ACTIONS as C,TOOLS as O,toolCallToInvoke as P}from"./tools.js";import{WebSocketServer as R,WebSocket as $}from"ws";const y=1024*1024,_=32*1024*1024,N=50,k=1e3,f=75e3,H="2024-11-05",U=256,T=new Set([...C,"event_tool_call","mcp_stdio_log"]);class W{server=null;port=0;authToken;invokeHandler=null;statusLineHandler=null;otelHandler=null;activeInvokes=0;wss=null;bridgeWs=null;mcpBridgeUpHandler=null;downstreamOutbox=[];mcpBridgeHttpActive=!1;pendingBridgeResponses=new Map;codebaseNavigatorCwd=null;codebaseNavigatorFacade=null;constructor(){this.authToken=M(32).toString("hex")}setInvokeHandler(e){this.invokeHandler=e}setStatusLineHandler(e){this.statusLineHandler=e}setOtelHandler(e){this.otelHandler=e}async start(e=0){return new Promise((t,r)=>{this.server=B.createServer((n,o)=>this.handleRequest(n,o)),this.wss=new R({noServer:!0}),this.server.on("upgrade",(n,o,s)=>{const i=new URL(n.url??"/",`http://127.0.0.1:${this.port}`);if(i.pathname!=="/mcp-bridge"||i.searchParams.get("token")!==this.authToken){o.destroy();return}this.wss.handleUpgrade(n,o,s,a=>{if(this.bridgeWs=a,h.info("internal-api","mcp bridge connected"),this.downstreamOutbox.length>0){const c=this.downstreamOutbox;this.downstreamOutbox=[];for(const p of c)a.send(p);h.info("internal-api",`mcp bridge flushed ${c.length} buffered downstream frame(s)`)}a.on("message",c=>{let p;try{p=JSON.parse(c.toString("utf8"))}catch{return}this.mcpBridgeUpHandler?.(p)}),a.on("close",()=>{this.bridgeWs===a&&(this.bridgeWs=null),h.info("internal-api","mcp bridge disconnected")}),a.on("error",()=>{})})}),this.server.on("error",r),this.server.listen(e,"127.0.0.1",()=>{const n=this.server.address();typeof n=="object"&&n&&(this.port=n.port),this.server.removeListener("error",r),t(this.port)})})}get url(){return`http://127.0.0.1:${this.port}?token=${this.authToken}`}get baseUrl(){return`http://127.0.0.1:${this.port}`}get mcpBridgeWsUrl(){return`ws://127.0.0.1:${this.port}/mcp-bridge?token=${this.authToken}`}get mcpBridgeHttpUrl(){return this.mcpBridgeHttpActive=!0,`${this.baseUrl}/mcp/app-bridge`}get mcpToolsHttpUrl(){return`${this.baseUrl}/mcp/tools`}get codebaseNavigatorHttpUrl(){return`${this.baseUrl}/mcp/codebase-navigator`}async setCodebaseNavigatorCwd(e){if(this.codebaseNavigatorCwd===e&&this.codebaseNavigatorFacade)return;this.codebaseNavigatorCwd=e;const t=this.codebaseNavigatorFacade;this.codebaseNavigatorFacade=null,await t?.close();const o=(await import(new URL("../../kimi-plugins/grix-layer/scripts/http-facade.mjs",import.meta.url).href)).createCodebaseNavigatorHttpFacade;this.codebaseNavigatorFacade=o({cwd:e})}setMcpBridgeUpHandler(e){this.mcpBridgeUpHandler=e}sendMcpFrameToBridge(e){const t=I(e);if(t!==void 0){const n=E(t),o=this.pendingBridgeResponses.get(n);if(o){this.pendingBridgeResponses.delete(n),clearTimeout(o.timer),o.resolve(e);return}}const r=JSON.stringify(e);if(this.bridgeWs&&this.bridgeWs.readyState===$.OPEN){this.bridgeWs.send(r);return}if(this.mcpBridgeHttpActive){h.debug("internal-api","dropping unsolicited MCP bridge frame without an HTTP stream");return}this.downstreamOutbox.length>=U&&(this.downstreamOutbox.shift(),h.warn("internal-api","mcp downstream outbox full, dropping oldest frame")),this.downstreamOutbox.push(r)}get token(){return this.authToken}async stop(){const e=this.codebaseNavigatorFacade;if(this.codebaseNavigatorFacade=null,await e?.close(),!!this.server){for(const[t,r]of this.pendingBridgeResponses)clearTimeout(r.timer),r.reject(new Error("internal API stopped")),this.pendingBridgeResponses.delete(t);return new Promise(t=>{this.server.close(()=>{this.server=null,t()})})}}checkAuth(e){return e.headers.authorization===`Bearer ${this.authToken}`||new URL(e.url??"/",`http://127.0.0.1:${this.port}`).searchParams.get("token")===this.authToken}handleRequest(e,t){if(e.method!=="POST"){t.writeHead(405),t.end(JSON.stringify({error:"method not allowed"}));return}if(!this.checkAuth(e)){t.writeHead(401),t.end(JSON.stringify({ok:!1,error:"unauthorized"}));return}const r=new URL(e.url??"/",`http://127.0.0.1:${this.port}`);if(r.pathname==="/mcp/tools"){this.handleMcpTools(e,t);return}if(r.pathname==="/mcp/app-bridge"){this.handleMcpBridgeHttp(e,t);return}if(r.pathname==="/mcp/codebase-navigator"){this.handleCodebaseNavigatorMcp(e,t);return}if(r.pathname==="/api/status-line"){this.handleStatusLine(e,t);return}if(r.pathname==="/v1/logs"||r.pathname==="/v1/traces"){this.handleOtel(r.pathname==="/v1/logs"?"logs":"traces",e,t);return}if(!r.pathname.startsWith("/api/invoke")){t.writeHead(404),t.end(JSON.stringify({error:"not found"}));return}const n=[];let o=0;e.on("data",s=>{if(o+=s.length,o>y){t.writeHead(413),t.end(JSON.stringify({ok:!1,error:"request body too large"})),e.destroy();return}n.push(s)}),e.on("end",async()=>{let s;try{s=JSON.parse(Buffer.concat(n).toString("utf8"))}catch{t.writeHead(400),t.end(JSON.stringify({ok:!1,error:"invalid JSON"}));return}const i=s.action,a=s.params??{};if(!i||!this.invokeHandler){t.writeHead(400),t.end(JSON.stringify({ok:!1,error:"missing action or no handler"}));return}if(!T.has(i)){t.writeHead(400),t.end(JSON.stringify({ok:!1,error:`action '${i}' is not allowed`}));return}if(this.activeInvokes>=N){t.writeHead(429),t.end(JSON.stringify({ok:!1,error:"too many concurrent invokes"}));return}const c=s.timeout_ms!=null?Math.max(k,Math.min(s.timeout_ms,f)):f,p=Date.now();this.activeInvokes++;const m=new AbortController;try{const g=await Promise.race([this.invokeHandler(i,a,m.signal,c),new Promise((w,J)=>setTimeout(()=>{m.abort(),J(new Error("invoke timeout"))},c))]);t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,data:g})),h.info("internal-api",`invoke ok action=${i} tool=${a.tool_name??"-"} ms=${Date.now()-p}`)}catch(g){m.abort();const w=g instanceof Error?g.message:String(g);t.writeHead(500),t.end(JSON.stringify({ok:!1,error:w})),h.error("internal-api",`invoke error action=${i} tool=${a.tool_name??"-"} ms=${Date.now()-p} err=${w}`)}finally{this.activeInvokes--}})}async handleMcpTools(e,t){let r;try{r=b(await v(e))}catch(i){l(t,null,-32700,i instanceof Error?i.message:String(i),400);return}if(r.method==="notifications/initialized"){t.writeHead(202),t.end();return}const n=r.id??null;if(r.method==="initialize"){u(t,n,{protocolVersion:H,capabilities:{tools:{}},serverInfo:{name:"grix-connector-tools",version:"1.0.0"}});return}if(r.method==="ping"){u(t,n,{});return}if(r.method==="tools/list"){u(t,n,{tools:O.map(({name:i,description:a,inputSchema:c})=>({name:i,description:a,inputSchema:c}))});return}if(r.method!=="tools/call"){l(t,n,-32601,`Method not found: ${r.method}`);return}const o=String(r.params?.name??""),s=r.params?.arguments??{};if(!O.some(i=>i.name===o)){l(t,n,-32602,`Unknown tool: ${o}`);return}try{const i=P(o,s),a=await this.invokeMcpTool(i.action,i.params,i.timeoutMs);u(t,n,{content:[{type:"text",text:JSON.stringify(a,null,2)??"null"}]})}catch(i){const a=i instanceof Error?i.message:String(i);u(t,n,{content:[{type:"text",text:JSON.stringify({error:a})}],isError:!0})}}async handleMcpBridgeHttp(e,t){let r;try{r=b(await v(e))}catch(o){l(t,null,-32700,o instanceof Error?o.message:String(o),400);return}if(!this.mcpBridgeUpHandler){l(t,r.id??null,-32603,"MCP bridge is unavailable",503);return}if(r.id===void 0){this.mcpBridgeUpHandler(r),t.writeHead(202),t.end();return}const n=E(r.id);if(this.pendingBridgeResponses.has(n)){l(t,r.id,-32600,"Duplicate in-flight MCP request id",409);return}try{const o=await new Promise((s,i)=>{const a=setTimeout(()=>{this.pendingBridgeResponses.delete(n),i(new Error("MCP bridge response timeout"))},f);this.pendingBridgeResponses.set(n,{resolve:s,reject:i,timer:a});try{this.mcpBridgeUpHandler(r)}catch(c){clearTimeout(a),this.pendingBridgeResponses.delete(n),i(c instanceof Error?c:new Error(String(c)))}});S(t,200,o)}catch(o){l(t,r.id,-32603,o instanceof Error?o.message:String(o),504)}}async handleCodebaseNavigatorMcp(e,t){let r;try{r=b(await v(e))}catch(a){l(t,null,-32700,a instanceof Error?a.message:String(a),400);return}if(r.method==="notifications/initialized"){t.writeHead(202),t.end();return}const n=r.id??null;if(r.method==="initialize"){u(t,n,{protocolVersion:H,capabilities:{tools:{}},serverInfo:{name:"codebase-navigator",version:"0.1.0"}});return}if(r.method==="ping"){u(t,n,{});return}let o;try{o=await this.getCodebaseNavigatorFacade()}catch(a){l(t,n,-32603,a instanceof Error?a.message:String(a),503);return}if(r.method==="tools/list"){u(t,n,{tools:o.listTools()});return}if(r.method!=="tools/call"){l(t,n,-32601,`Method not found: ${r.method}`);return}const s=String(r.params?.name??""),i=r.params?.arguments??{};if(!o.listTools().some(a=>a.name===s)){l(t,n,-32602,`Unknown tool: ${s}`);return}try{u(t,n,await o.callTool(s,i))}catch(a){u(t,n,{content:[{type:"text",text:JSON.stringify({error:a instanceof Error?a.message:String(a)})}],isError:!0})}}async getCodebaseNavigatorFacade(){if(this.codebaseNavigatorFacade)return this.codebaseNavigatorFacade;if(!this.codebaseNavigatorCwd)throw new Error("codebase navigator cwd is not configured");if(await this.setCodebaseNavigatorCwd(this.codebaseNavigatorCwd),!this.codebaseNavigatorFacade)throw new Error("codebase navigator failed to initialize");return this.codebaseNavigatorFacade}async invokeMcpTool(e,t,r){if(!this.invokeHandler)throw new Error("invoke handler is unavailable");if(!T.has(e))throw new Error(`action '${e}' is not allowed`);if(this.activeInvokes>=N)throw new Error("too many concurrent invokes");const n=r!=null?Math.max(k,Math.min(r,f)):f,o=new AbortController;let s;this.activeInvokes++;try{return await Promise.race([this.invokeHandler(e,t,o.signal,n),new Promise((i,a)=>{s=setTimeout(()=>{o.abort(),a(new Error("invoke timeout"))},n)})])}finally{s&&clearTimeout(s),this.activeInvokes--}}handleStatusLine(e,t){const r=[];let n=0;e.on("data",o=>{if(n+=o.length,n>y){t.writeHead(413),t.end(JSON.stringify({ok:!1,error:"request body too large"})),e.destroy();return}r.push(o)}),e.on("end",async()=>{let o;try{o=JSON.parse(Buffer.concat(r).toString("utf8"))}catch{t.writeHead(400),t.end(JSON.stringify({ok:!1,error:"invalid JSON"}));return}if(!this.statusLineHandler){t.writeHead(200),t.end(JSON.stringify({ok:!0}));return}try{await this.statusLineHandler(o),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0}))}catch(s){console.error("[internal-api] statusLine handler error:",s instanceof Error?s.message:s),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0}))}})}handleOtel(e,t,r){const n=[];let o=0,s=!1;t.on("data",i=>{if(!s){if(o+=i.length,o>_){s=!0,r.writeHead(413,{"content-type":"application/json"}),r.end(JSON.stringify({error:"OTLP request body too large"}));return}n.push(i)}}),t.on("end",async()=>{if(s)return;const i=Buffer.concat(n).toString("utf8");let a;try{const c=JSON.parse(i);if(!c||typeof c!="object"||Array.isArray(c))throw new Error("OTLP body is not an object");a=c}catch{r.writeHead(400,{"content-type":"application/json"}),r.end(JSON.stringify({error:"invalid OTLP JSON"}));return}try{await this.otelHandler?.(e,a,i)}catch(c){h.warn("internal-api",`OTLP ${e} evidence persistence failed: ${c instanceof Error?c.message:String(c)}`)}r.writeHead(200,{"content-type":"application/json"}),r.end("{}")})}}function v(d){return new Promise((e,t)=>{const r=[];let n=0,o=!1;d.on("data",s=>{if(!o){if(n+=s.length,n>y){o=!0,t(new Error("request body too large")),d.resume();return}r.push(s)}}),d.on("end",()=>{if(!o){o=!0;try{e(JSON.parse(Buffer.concat(r).toString("utf8")))}catch{t(new Error("invalid JSON"))}}}),d.on("error",s=>{o||(o=!0,t(s))})})}function b(d){if(!d||typeof d!="object"||Array.isArray(d))throw new Error("invalid JSON-RPC request");const e=d;if(e.jsonrpc!=="2.0"||typeof e.method!="string")throw new Error("invalid JSON-RPC request");const t=e.id;if(t!=null&&typeof t!="string"&&typeof t!="number")throw new Error("invalid JSON-RPC id");return d}function I(d){if(!d||typeof d!="object"||Array.isArray(d)||!Object.prototype.hasOwnProperty.call(d,"id"))return;const e=d.id;if(e===null||typeof e=="string"||typeof e=="number")return e}function E(d){return`${typeof d}:${String(d)}`}function S(d,e,t){d.writeHead(e,{"content-type":"application/json"}),d.end(JSON.stringify(t))}function u(d,e,t){S(d,200,{jsonrpc:"2.0",id:e,result:t})}function l(d,e,t,r,n=200){S(d,n,{jsonrpc:"2.0",id:e,error:{code:t,message:r}})}export{W as InternalApiServer};
|
package/dist/core/mcp/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$",description:"Peer user ID (session_search only): locate the private chat between the current user and this account exactly, independent of title/nickname/remark. Takes precedence over id/keyword. Use this when you know the target user's ID (e.g. the agent owner). Returns error 4004 when no such private chat exists."},keyword:{type:"string",description:"Search keyword."},sessionType:{type:"integer",enum:[1,2],description:"Filter by session type (session_search only): 1=private chat, 2=group chat. Omit to return all."},limit:{type:"integer",description:"Max results. message_history defaults to 1 when omitted."},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","search_favorite_sessions"]},id:{type:"string"},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$"},keyword:{type:"string",maxLength:200},sessionType:{type:"integer",enum:[1,2]},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_audit_data",description:"Read connector-backed conversation audit replay data from Grix/AIBot: manifest, span pages, and content chunks.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"],description:"Audit read action. Start with get_manifest, then call list_spans and/or get_content_chunk based on the manifest."},auditId:{type:"string",description:"Audit ID returned by the audited turn metadata."},revision:{type:"integer",description:"Optional manifest revision to read. Usually use the revision returned by get_manifest."},cursor:{type:"string",description:"Opaque pagination cursor returned by list_spans or get_content_chunk."},limit:{type:"integer",description:"Max span items for list_spans."},contentId:{type:"string",description:"Content reference ID from manifest.content_refs (get_content_chunk only)."},maxBytes:{type:"integer",description:"Max UTF-8 bytes to return for get_content_chunk."}},required:["action","auditId"]},validation:{required:["action","auditId"],properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"]},auditId:{type:"string"},revision:{type:"integer",minimum:1},cursor:{type:"string",maxLength:4096},limit:{type:"integer",minimum:1,maximum:200},contentId:{type:"string"},maxBytes:{type:"integer",minimum:1,maximum:131072}}}},{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. Do NOT use this to answer the inbound event you are currently handling \u2014 answer in plain text (or the reply tool where available) instead, otherwise the user receives duplicate messages.",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. The link is reachable only inside the tailnet, so just send it as-is \u2014 no need to worry about or mention any link lifetime. 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. Leave unset to use the long default; set only if you deliberately want a short-lived link."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_file_upload",description:"Upload a local file to the Grix platform and send it as a media message in the target session. Supports images (jpg/png/webp/gif/bmp/heic/heif), videos (mp4/mov/m4v/webm/mkv/avi), documents (pdf/doc/docx/xls/xlsx/ppt/pptx/txt/md/csv/json/xml), and archives (zip/rar/7z/tar/gz). Max 50 MB. Use this instead of grix_file_link when the file should appear as a native attachment in the chat (visible inline for images/videos), rather than a tailnet download link.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file to upload."},session_id:{type:"string",description:"Target session ID to send the file to."},caption:{type:"string",description:"Optional text caption for the media message."},reply_to_message_id:{type:"string",description:"Optional message ID to quote/reply to."}},required:["file_path","session_id"]},validation:{required:["file_path","session_id"],properties:{file_path:{type:"string",maxLength:4096},session_id:{type:"string"},caption:{type:"string",maxLength:2e3},reply_to_message_id:{type:"string"}}}},{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:"Professional behavioral introduction describing the Agent purpose, responsibilities, operating expectations, and boundaries (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 display name and/or text introduction of one of your owner's agents, identified by its numeric agent ID. Provide agent_name, introduction, or both \u2014 at least one is required.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},agent_name:{type:"string",description:"New display name (max 100 characters, must be unique among the owner's agents)."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id"]},validation:{required:["agent_id"],properties:{agent_id:{type:"string"},agent_name:{type:"string",maxLength:100},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 creates a NEW private session between the owner and that agent for each dispatch (it does not reuse past sessions), 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. Provide a short title summarizing the core of the task \u2014 it becomes the new session's title; if omitted the backend derives one from the task text.`,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."},title:{type:"string",description:"Optional short title (a few words) summarizing the core of the task; used as the new session's title. If omitted, the backend derives a title from the task text."}},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},title:{type:"string",maxLength:255}}}},{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). Dispatch callbacks (report_dispatch_result) MUST use this tool, even when you are a member of the callback session: a message sent as yourself cannot quote-wake the dispatcher agent, and your membership is not checked. NEVER use this for your own ordinary replies in a conversation \u2014 that would make your words show up as the owner's message; reply normally (or use grix_message_send to send as yourself) for those. The owner must be a member of the target session. Requires the Send as Owner permission scope \u2014 if rejected for missing permission, surface the error as-is so the owner can grant it; do not fall back to grix_message_send. Optional quoted_message_id quotes a message in the same session (used by dispatch callbacks to wake the dispatcher agent).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."},quoted_message_id:{type:"string",description:"Optional message ID in the target session to quote/reply to.",pattern:"^[1-9]\\d*$"}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4},quoted_message_id:{type:"string",pattern:"^[1-9]\\d*$"}}}},{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). When session_id identifies one completed session, the entry also includes exactly one clean final_result; non-completed and list queries never include message content. 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:{}}},{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_webhook_create",description:'Create a webhook endpoint for a session this agent belongs to. POSTing {"content":"..."} to the returned URL sends that text into the session as the owner and wakes the agent \u2014 use it for human/timer-driven scheduled or recurring triggers (cron, schtasks, launchd). Not for agent-to-agent dispatch callbacks.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID the webhook posts into (required). Must be a session this agent is a member of \u2014 normally the current chat_id."},expires_at:{type:"string",description:"Optional RFC3339 expiry; omit for a non-expiring endpoint."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"},expires_at:{type:"string"}}}},{name:"grix_webhook_list",description:"List the active webhook endpoints (with full URLs) of a session this agent belongs to. Call this BEFORE grix_webhook_create and reuse an existing endpoint instead of creating another one.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID (required). Must be a session this agent is a member of \u2014 normally the current chat_id."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_webhook_delete",description:"Delete a webhook endpoint by id (from grix_webhook_list / grix_webhook_create). Only endpoints of sessions this agent belongs to. Use when tearing down scheduled triggers or removing duplicate endpoints.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Webhook endpoint id (required, pass as string)."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"}}}},{name:"grix_widget_visitor_ban",description:"Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Widget visitor session ID (required)."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_egg_search",description:"Search the Grix egg market (published skill/persona packages the owner can hatch into a new agent or install into an existing one). Keyword matching is per-term AND over name + description + category, so search short keywords (one concept per call) rather than a whole sentence; omit keyword to browse a category. Results include id, name, description, category, install_count, can_create_agent, and existing_agent_client_types.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Short search keyword(s); terms are ANDed. Omit to list by category."},categoryId:{type:"string",description:"Optional egg category ID to restrict results."},locale:{type:"string",description:"Optional locale such as zh-CN or en; defaults to the owner's locale."},page:{type:"integer",description:"Page number, starting from 1."},pageSize:{type:"integer",description:"Items per page (1-50), default 20."}}},validation:{required:[],properties:{keyword:{type:"string",maxLength:200},categoryId:{type:"string"},locale:{type:"string",maxLength:16},page:{type:"integer",minimum:1},pageSize:{type:"integer",minimum:1,maximum:50}}}},{name:"grix_egg_get",description:"Get one Grix egg by id \u2014 full description, current version, and whether it can create a new agent or install into existing agents. Use after grix_egg_search to show details before the owner decides to hatch.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Egg ID from grix_egg_search."},locale:{type:"string",description:"Optional locale."},version:{type:"integer",description:"Optional specific version; defaults to the current one."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"},locale:{type:"string",maxLength:16},version:{type:"integer",minimum:1}}}},{name:"grix_skill_set",description:"Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (max 100 characters); unique within the owner. Used as the identifier and the synced directory name."},content:{type:"string",description:"Full SKILL.md text. Pass an empty string to delete the skill by name."}},required:["name","content"]},validation:{required:["name","content"],properties:{name:{type:"string",maxLength:100},content:{type:"string",maxLength:262144}}}},{name:"grix_skill_get",description:"Read one of your owner's custom skills by name (returns its full SKILL.md content), or list the owner's skill library when no name is given. Use this before editing to fetch current content, then call grix_skill_set with the revised text.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name to fetch. Omit to list all skills (names + versions) instead."}}},validation:{required:[],properties:{name:{type:"string",maxLength:100}}}}],a=[{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_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],w={grix_dispatch_agent:"grix-agent-dispatch",grix_agent_update:"grix-agent-dispatch",grix_query:"grix-query",grix_audit_data:"grix-audit-data",grix_group:"grix-group",grix_message_send:"message-send",grix_message_unsend:"message-unsend",grix_admin:"grix-admin",grix_call_owner:"grix-owner-relay",grix_session_send:"grix-owner-relay",grix_chat_state_query:"grix-chat-state",grix_chat_state_update:"grix-chat-state",grix_status:"grix-chat-state",grix_access_control:"grix-access-control",grix_widget_visitor_ban:"grix-widget-visitor-ban",grix_webhook_create:"grix-scheduled-trigger",grix_webhook_list:"grix-scheduled-trigger",grix_webhook_delete:"grix-scheduled-trigger",grix_file_link:"tailnet-file-share"};function v(e){return` Before calling this tool, follow the \`${e}\` skill's procedure first; do not invoke this tool directly without going through that skill's guidance.`}for(const e of[...r,...a]){const t=w[e.name];t&&(e.description+=v(t))}const 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)),x=new Set(r.map(e=>e.name)),k=new Set(a.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,S=/[A-Za-z0-9._-]+/;function V(e){return!!(x.has(e)||k.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const q=[...r,...a],A=[...q,...p],X=new Map(A.map(e=>[e.name,e]));function Y(e,t){return e==="reply"?{name:"grix_reply",args:g("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:g("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(I)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return _(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(S)?.[0]}function _(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function g(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=_(i),d=String(n.session_id??""),s=u(n.session_id),b=/\bevent_id\b|["'<>\s]/.test(d);s&&!(b&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function J(e){return c.has(e)}function ee(e,t){switch(e){case"grix_query":return T(t);case"grix_audit_data":return D(t);case"grix_group":return L(t);case"grix_message_send":return E(t);case"grix_message_unsend":return j(t);case"grix_file_link":return N(t);case"grix_file_upload":return C(t);case"grix_admin":return K(t);case"grix_call_owner":return P(t);case"grix_agent_update":return U(t);case"grix_dispatch_agent":return z(t);case"grix_session_send":return R(t);case"grix_chat_state_query":return G(t);case"grix_chat_state_update":return $(t);case"grix_access_control":return B(t);case"grix_widget_visitor_ban":return W(t);case"grix_webhook_create":return F(t);case"grix_webhook_list":return{action:"webhook_list",params:{session_id:t.session_id}};case"grix_webhook_delete":return{action:"webhook_delete",params:{id:t.id}};case"grix_egg_search":return O(t);case"grix_egg_get":return M(t);case"grix_skill_set":return Q(t);case"grix_skill_get":return H(t);default:throw new Error(`Unknown tool: ${e}`)}}const m={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search",search_favorite_sessions:"search_favorite_sessions"};function T(e){const t=String(e.action??""),n=m[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.peerId!=null&&(i.peer_id=e.peerId),e.keyword!=null&&(i.keyword=e.keyword),e.sessionType!=null&&(i.session_type=e.sessionType),e.limit!=null?i.limit=e.limit:t==="message_history"&&(i.limit=1),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}}function O(e){const t={};return e.keyword!=null&&(t.keyword=e.keyword),e.categoryId!=null&&(t.category_id=e.categoryId),e.locale!=null&&(t.locale=e.locale),e.page!=null&&(t.page=e.page),e.pageSize!=null&&(t.page_size=e.pageSize),{action:"egg_search",params:t}}function M(e){const t={id:e.id};return e.locale!=null&&(t.locale=e.locale),e.version!=null&&(t.version=e.version),{action:"egg_get",params:t}}const h={get_manifest:"audit_get_manifest",list_spans:"audit_list_spans",get_content_chunk:"audit_get_content_chunk"};function D(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_audit_data action: ${t}`);const i={audit_id:e.auditId};return e.revision!=null&&(i.revision=e.revision),e.cursor!=null&&(i.cursor=e.cursor),e.limit!=null&&(i.limit=e.limit),e.contentId!=null&&(i.content_id=e.contentId),e.maxBytes!=null&&(i.max_bytes=e.maxBytes),{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 L(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 E(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 j(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function N(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}function C(e){const t={file_path:e.file_path,session_id:e.session_id};return e.caption!=null&&(t.caption=e.caption),e.reply_to_message_id!=null&&(t.reply_to_message_id=e.reply_to_message_id),{action:"file_upload",params:t,timeoutMs:6e4}}const f={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 P(e){return{action:"call_owner",params:{session_id:e.session_id}}}function U(e){if(e.agent_name==null&&e.introduction==null)throw new Error("grix_agent_update requires agent_name or introduction");const t={agent_id:e.agent_id};return e.agent_name!=null&&(t.agent_name=e.agent_name),e.introduction!=null&&(t.introduction=e.introduction),{action:"agent_introduction_update",params:t}}function z(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task,title:e.title},timeoutMs:75e3}}function R(e){const t={session_id:e.session_id,content:e.content};return e.quoted_message_id!=null&&String(e.quoted_message_id).trim()!==""&&(t.quoted_message_id=e.quoted_message_id),{action:"session_send",params:t}}function G(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 B(e){const t=String(e.action??""),n=Z[t];if(!n)throw new Error(`Unknown grix_access_control action: ${t}`);const i={};return e.code!=null&&(i.code=e.code),e.sender_id!=null&&(i.sender_id=e.sender_id),e.policy!=null&&(i.policy=e.policy),{action:"claude_access_control",params:{verb:n,payload:i},timeoutMs:3e4}}function W(e){return{action:"widget_visitor_ban",params:{session_id:e.session_id}}}function F(e){const t={session_id:e.session_id};return e.expires_at!=null&&(t.expires_at=e.expires_at),{action:"webhook_create",params:t}}function $(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 Q(e){return{action:"skill_set",params:{name:e.name,content:e.content},timeoutMs:3e4}}function H(e){const t={};return e.name!=null&&(t.name=e.name),{action:"skill_get",params:t}}function K(e){const t=String(e.action??""),n=f[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 te=new Set([...Object.values(m),...Object.values(h),...Object.values(y),...Object.values(f),"send_msg","delete_msg","file_link","file_upload","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update","claude_access_control","widget_visitor_ban","webhook_create","webhook_list","webhook_delete","skill_set","skill_get"]),Z={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Z as ACCESS_CONTROL_ACTION_MAP,q as ALL_TOOLS,a as EVENT_TOOLS,A as EXPOSED_TOOLS,te as PHASE1_INVOKE_ACTIONS,x as PHASE1_TOOL_NAMES,k as PHASE2_TOOL_NAMES,r as TOOLS,p as TOOL_ALIASES,X as TOOL_MAP,J as isAlias,V as isGrixInternalToolName,Y as mapToolAlias,g as normalizeEventToolArgs,ee as toolCallToInvoke};
|
|
1
|
+
const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$",description:"Peer user ID (session_search only): locate the private chat between the current user and this account exactly, independent of title/nickname/remark. Takes precedence over id/keyword. Use this when you know the target user's ID (e.g. the agent owner). Returns error 4004 when no such private chat exists."},keyword:{type:"string",description:"Search keyword."},sessionType:{type:"integer",enum:[1,2],description:"Filter by session type (session_search only): 1=private chat, 2=group chat. Omit to return all."},limit:{type:"integer",description:"Max results. message_history defaults to 1 when omitted."},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","search_favorite_sessions"]},id:{type:"string"},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$"},keyword:{type:"string",maxLength:200},sessionType:{type:"integer",enum:[1,2]},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_audit_data",description:"Read connector-backed conversation audit replay data from Grix/AIBot: manifest, span pages, and content chunks.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"],description:"Audit read action. Start with get_manifest, then call list_spans and/or get_content_chunk based on the manifest."},auditId:{type:"string",description:"Audit ID returned by the audited turn metadata."},revision:{type:"integer",description:"Optional manifest revision to read. Usually use the revision returned by get_manifest."},cursor:{type:"string",description:"Opaque pagination cursor returned by list_spans or get_content_chunk."},limit:{type:"integer",description:"Max span items for list_spans."},contentId:{type:"string",description:"Content reference ID from manifest.content_refs (get_content_chunk only)."},maxBytes:{type:"integer",description:"Max UTF-8 bytes to return for get_content_chunk."}},required:["action","auditId"]},validation:{required:["action","auditId"],properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"]},auditId:{type:"string"},revision:{type:"integer",minimum:1},cursor:{type:"string",maxLength:4096},limit:{type:"integer",minimum:1,maximum:200},contentId:{type:"string"},maxBytes:{type:"integer",minimum:1,maximum:131072}}}},{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. Do NOT use this to answer the inbound event you are currently handling \u2014 answer in plain text (or the reply tool where available) instead, otherwise the user receives duplicate messages.",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. The link is reachable only inside the tailnet, so just send it as-is \u2014 no need to worry about or mention any link lifetime. 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. Leave unset to use the long default; set only if you deliberately want a short-lived link."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_file_upload",description:"Upload a local file to the Grix platform and send it as a media message in the target session. Supports images (jpg/png/webp/gif/bmp/heic/heif), videos (mp4/mov/m4v/webm/mkv/avi), documents (pdf/doc/docx/xls/xlsx/ppt/pptx/txt/md/csv/json/xml), and archives (zip/rar/7z/tar/gz). Max 50 MB. Use this instead of grix_file_link when the file should appear as a native attachment in the chat (visible inline for images/videos), rather than a tailnet download link.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file to upload."},session_id:{type:"string",description:"Target session ID to send the file to."},caption:{type:"string",description:"Optional text caption for the media message."},reply_to_message_id:{type:"string",description:"Optional message ID to quote/reply to."}},required:["file_path","session_id"]},validation:{required:["file_path","session_id"],properties:{file_path:{type:"string",maxLength:4096},session_id:{type:"string"},caption:{type:"string",maxLength:2e3},reply_to_message_id:{type:"string"}}}},{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:"Professional behavioral introduction describing the Agent purpose, responsibilities, operating expectations, and boundaries (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 display name and/or text introduction of one of your owner's agents, identified by its numeric agent ID. Provide agent_name, introduction, or both \u2014 at least one is required.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},agent_name:{type:"string",description:"New display name (max 100 characters, must be unique among the owner's agents)."},introduction:{type:"string",description:"New text introduction (max 3072 characters)."}},required:["agent_id"]},validation:{required:["agent_id"],properties:{agent_id:{type:"string"},agent_name:{type:"string",maxLength:100},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 creates a NEW private session between the owner and that agent for each dispatch (it does not reuse past sessions), 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. Provide a short title summarizing the core of the task \u2014 it becomes the new session's title; if omitted the backend derives one from the task text.`,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."},title:{type:"string",description:"Optional short title (a few words) summarizing the core of the task; used as the new session's title. If omitted, the backend derives a title from the task text."}},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},title:{type:"string",maxLength:255}}}},{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). Dispatch callbacks (report_dispatch_result) MUST use this tool, even when you are a member of the callback session: a message sent as yourself cannot quote-wake the dispatcher agent, and your membership is not checked. NEVER use this for your own ordinary replies in a conversation \u2014 that would make your words show up as the owner's message; reply normally (or use grix_message_send to send as yourself) for those. The owner must be a member of the target session. Requires the Send as Owner permission scope \u2014 if rejected for missing permission, surface the error as-is so the owner can grant it; do not fall back to grix_message_send. Optional quoted_message_id quotes a message in the same session (used by dispatch callbacks to wake the dispatcher agent).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."},quoted_message_id:{type:"string",description:"Optional message ID in the target session to quote/reply to.",pattern:"^[1-9]\\d*$"}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4},quoted_message_id:{type:"string",pattern:"^[1-9]\\d*$"}}}},{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). When session_id identifies one completed session, the entry also includes exactly one clean final_result; non-completed and list queries never include message content. 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:{}}},{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_webhook_create",description:'Create a webhook endpoint for a session this agent belongs to. POSTing {"content":"..."} to the returned URL sends that text into the session as the owner and wakes the agent \u2014 use it for human/timer-driven scheduled or recurring triggers (cron, schtasks, launchd). Not for agent-to-agent dispatch callbacks.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID the webhook posts into (required). Must be a session this agent is a member of \u2014 normally the current chat_id."},expires_at:{type:"string",description:"Optional RFC3339 expiry; omit for a non-expiring endpoint."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"},expires_at:{type:"string"}}}},{name:"grix_webhook_list",description:"List the active webhook endpoints (with full URLs) of a session this agent belongs to. Call this BEFORE grix_webhook_create and reuse an existing endpoint instead of creating another one.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID (required). Must be a session this agent is a member of \u2014 normally the current chat_id."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_webhook_delete",description:"Delete a webhook endpoint by id (from grix_webhook_list / grix_webhook_create). Only endpoints of sessions this agent belongs to. Use when tearing down scheduled triggers or removing duplicate endpoints.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Webhook endpoint id (required, pass as string)."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"}}}},{name:"grix_widget_visitor_ban",description:"Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Widget visitor session ID (required)."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_egg_search",description:"Search the Grix egg market (published skill/persona packages the owner can hatch into a new agent or install into an existing one). Keyword matching is per-term AND over name + description + category, so search short keywords (one concept per call) rather than a whole sentence; omit keyword to browse a category. Results include id, name, description, category, install_count, can_create_agent, and existing_agent_client_types.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Short search keyword(s); terms are ANDed. Omit to list by category."},categoryId:{type:"string",description:"Optional egg category ID to restrict results."},locale:{type:"string",description:"Optional locale such as zh-CN or en; defaults to the owner's locale."},page:{type:"integer",description:"Page number, starting from 1."},pageSize:{type:"integer",description:"Items per page (1-50), default 20."}}},validation:{required:[],properties:{keyword:{type:"string",maxLength:200},categoryId:{type:"string"},locale:{type:"string",maxLength:16},page:{type:"integer",minimum:1},pageSize:{type:"integer",minimum:1,maximum:50}}}},{name:"grix_egg_get",description:"Get one Grix egg by id \u2014 full description, current version, and whether it can create a new agent or install into existing agents. Use after grix_egg_search to show details before the owner decides to hatch.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Egg ID from grix_egg_search."},locale:{type:"string",description:"Optional locale."},version:{type:"integer",description:"Optional specific version; defaults to the current one."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"},locale:{type:"string",maxLength:16},version:{type:"integer",minimum:1}}}},{name:"grix_skill_set",description:"Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (max 100 characters); unique within the owner. Used as the identifier and the synced directory name."},content:{type:"string",description:"Full SKILL.md text. Pass an empty string to delete the skill by name."}},required:["name","content"]},validation:{required:["name","content"],properties:{name:{type:"string",maxLength:100},content:{type:"string",maxLength:262144}}}},{name:"grix_skill_get",description:"Read one of your owner's custom skills by name (returns its full SKILL.md content), or list the owner's skill library when no name is given. Use this before editing to fetch current content, then call grix_skill_set with the revised text.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name to fetch. Omit to list all skills (names + versions) instead."}}},validation:{required:[],properties:{name:{type:"string",maxLength:100}}}}],a=[{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_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],w={grix_dispatch_agent:"grix-agent-dispatch",grix_agent_update:"grix-agent-dispatch",grix_query:"grix-query",grix_audit_data:"grix-audit-data",grix_group:"grix-group",grix_message_send:"message-send",grix_message_unsend:"message-unsend",grix_admin:"grix-admin",grix_call_owner:"grix-owner-relay",grix_session_send:"grix-owner-relay",grix_chat_state_query:"grix-chat-state",grix_chat_state_update:"grix-chat-state",grix_status:"grix-chat-state",grix_access_control:"grix-access-control",grix_widget_visitor_ban:"grix-widget-visitor-ban",grix_webhook_create:"grix-scheduled-trigger",grix_webhook_list:"grix-scheduled-trigger",grix_webhook_delete:"grix-scheduled-trigger",grix_file_link:"tailnet-file-share"};function v(e){return` Before calling this tool, follow the \`${e}\` skill's procedure first; do not invoke this tool directly without going through that skill's guidance.`}for(const e of[...r,...a]){const t=w[e.name];t&&(e.description+=v(t))}const 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)),x=new Set(r.map(e=>e.name)),k=new Set(a.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,S=/[A-Za-z0-9._-]+/;function V(e){return!!(x.has(e)||k.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const q=[...r,...a],A=[...q,...p],X=new Map(A.map(e=>[e.name,e]));function Y(e,t){return e==="reply"?{name:"grix_reply",args:g("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:g("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(I)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return _(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(S)?.[0]}function _(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function g(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=_(i),d=String(n.session_id??""),s=u(n.session_id),b=/\bevent_id\b|["'<>\s]/.test(d);s&&!(b&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function J(e){return c.has(e)}function ee(e,t){switch(e){case"grix_query":return T(t);case"grix_audit_data":return D(t);case"grix_group":return L(t);case"grix_message_send":return E(t);case"grix_message_unsend":return j(t);case"grix_file_link":return N(t);case"grix_file_upload":return C(t);case"grix_admin":return K(t);case"grix_call_owner":return P(t);case"grix_agent_update":return U(t);case"grix_dispatch_agent":return z(t);case"grix_session_send":return R(t);case"grix_chat_state_query":return G(t);case"grix_chat_state_update":return $(t);case"grix_access_control":return B(t);case"grix_widget_visitor_ban":return W(t);case"grix_webhook_create":return F(t);case"grix_webhook_list":return{action:"webhook_list",params:{session_id:t.session_id}};case"grix_webhook_delete":return{action:"webhook_delete",params:{id:t.id}};case"grix_egg_search":return O(t);case"grix_egg_get":return M(t);case"grix_skill_set":return Q(t);case"grix_skill_get":return H(t);default:throw new Error(`Unknown tool: ${e}`)}}const m={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search",search_favorite_sessions:"search_favorite_sessions"};function T(e){const t=String(e.action??""),n=m[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.peerId!=null&&(i.peer_id=e.peerId),e.keyword!=null&&(i.keyword=e.keyword),e.sessionType!=null&&(i.session_type=e.sessionType),e.limit!=null?i.limit=e.limit:t==="message_history"&&(i.limit=1),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}}function O(e){const t={};return e.keyword!=null&&(t.keyword=e.keyword),e.categoryId!=null&&(t.category_id=e.categoryId),e.locale!=null&&(t.locale=e.locale),e.page!=null&&(t.page=e.page),e.pageSize!=null&&(t.page_size=e.pageSize),{action:"egg_search",params:t}}function M(e){const t={id:e.id};return e.locale!=null&&(t.locale=e.locale),e.version!=null&&(t.version=e.version),{action:"egg_get",params:t}}const h={get_manifest:"audit_get_manifest",list_spans:"audit_list_spans",get_content_chunk:"audit_get_content_chunk"};function D(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_audit_data action: ${t}`);const i={audit_id:e.auditId};return e.revision!=null&&(i.revision=e.revision),e.cursor!=null&&(i.cursor=e.cursor),e.limit!=null&&(i.limit=e.limit),e.contentId!=null&&(i.content_id=e.contentId),e.maxBytes!=null&&(i.max_bytes=e.maxBytes),{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 L(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 E(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 j(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function N(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}function C(e){const t={file_path:e.file_path,session_id:e.session_id};return e.caption!=null&&(t.caption=e.caption),e.reply_to_message_id!=null&&(t.reply_to_message_id=e.reply_to_message_id),{action:"file_upload",params:t,timeoutMs:6e4}}const f={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 P(e){return{action:"call_owner",params:{session_id:e.session_id}}}function U(e){if(e.agent_name==null&&e.introduction==null)throw new Error("grix_agent_update requires agent_name or introduction");const t={agent_id:e.agent_id};return e.agent_name!=null&&(t.agent_name=e.agent_name),e.introduction!=null&&(t.introduction=e.introduction),{action:"agent_introduction_update",params:t}}function z(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task,title:e.title},timeoutMs:75e3}}function R(e){const t={session_id:e.session_id,content:e.content};return e.quoted_message_id!=null&&String(e.quoted_message_id).trim()!==""&&(t.quoted_message_id=e.quoted_message_id),{action:"session_send",params:t}}function G(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 B(e){const t=String(e.action??""),n=Z[t];if(!n)throw new Error(`Unknown grix_access_control action: ${t}`);const i={};return e.code!=null&&(i.code=e.code),e.sender_id!=null&&(i.sender_id=e.sender_id),e.policy!=null&&(i.policy=e.policy),{action:"claude_access_control",params:{verb:n,payload:i},timeoutMs:3e4}}function W(e){return{action:"widget_visitor_ban",params:{session_id:e.session_id}}}function F(e){const t={session_id:e.session_id};return e.expires_at!=null&&(t.expires_at=e.expires_at),{action:"webhook_create",params:t}}function $(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 Q(e){return{action:"skill_set",params:{name:e.name,content:e.content},timeoutMs:3e4}}function H(e){const t={};return e.name!=null&&(t.name=e.name),{action:"skill_get",params:t}}function K(e){const t=String(e.action??""),n=f[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 te=new Set([...Object.values(m),...Object.values(h),...Object.values(y),...Object.values(f),"send_msg","delete_msg","file_link","file_upload","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update","claude_access_control","widget_visitor_ban","webhook_create","webhook_list","webhook_delete","skill_set","skill_get","egg_search","egg_get"]),Z={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Z as ACCESS_CONTROL_ACTION_MAP,q as ALL_TOOLS,a as EVENT_TOOLS,A as EXPOSED_TOOLS,te as PHASE1_INVOKE_ACTIONS,x as PHASE1_TOOL_NAMES,k as PHASE2_TOOL_NAMES,r as TOOLS,p as TOOL_ALIASES,X as TOOL_MAP,J as isAlias,V as isGrixInternalToolName,Y as mapToolAlias,g as normalizeEventToolArgs,ee as toolCallToInvoke};
|
|
@@ -254,7 +254,7 @@ Provide at least one of `agent_name` / `introduction`.
|
|
|
254
254
|
- `agent_id` (required) — target agent's numeric ID, as a string.
|
|
255
255
|
- `agent_name` (optional) — new display name (max 100 chars); must be unique
|
|
256
256
|
among the owner's agents.
|
|
257
|
-
- `introduction` (optional) — new introduction text (max
|
|
257
|
+
- `introduction` (optional) — new introduction text (max 3072 chars).
|
|
258
258
|
|
|
259
259
|
Renaming only changes the platform-side display name; local connector config
|
|
260
260
|
(`agents.json` entry names) is untouched.
|
package/dist/mcp/stdio/server.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
`),c.exit(1));const
|
|
4
|
-
`),await new Promise(i=>setTimeout(i,s)),
|
|
5
|
-
`)}}async function
|
|
6
|
-
`)
|
|
7
|
-
`)}),
|
|
8
|
-
`)})
|
|
9
|
-
`)
|
|
2
|
+
import R from"node:http";import I from"node:net";import c from"node:process";import{Server as A}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as x}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as $,ListToolsRequestSchema as C}from"@modelcontextprotocol/sdk/types.js";import{toolCallToInvoke as P,mapToolAlias as k,EXPOSED_TOOLS as L,TOOL_MAP as b,PHASE2_TOOL_NAMES as M}from"../../core/mcp/tools.js";function E(e,t){const o=c.argv.slice(2);for(let n=0;n<o.length;n++)if(o[n]===`--${e}`&&o[n+1])return o[n+1];return c.env[t]||void 0}const O=E("handle-url","GRIX_CONNECTOR_INTERNAL_API")??"";O||(c.stderr.write(`FATAL: --handle-url <url> required (or set GRIX_CONNECTOR_INTERNAL_API)
|
|
3
|
+
`),c.exit(1));const q=parseInt(E("notify-port","GRIX_MCP_NOTIFY_PORT")??"0",10),u=[1e3,2e3,4e3];function D(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 m(e,t,o=15e3,n=0){try{return await F(e,t,o)}catch(r){if(n>0&&D(r)){const s=u[u.length-n]??u[u.length-1];return c.stderr.write(`[grix-stdio] invokeApi retry (${u.length-n+1}/${u.length}) action=${e} after ${s}ms: ${r instanceof Error?r.message:r}
|
|
4
|
+
`),await new Promise(i=>setTimeout(i,s)),m(e,t,o,n-1)}throw r}}function F(e,t,o){const n=new URL(O),r=new URL("/api/invoke",n),s=n.searchParams.get("token")??"";s&&r.searchParams.set("token",s);const i=JSON.stringify({action:e,params:t,timeout_ms:o});return new Promise((l,a)=>{const N=setTimeout(()=>a(new Error("invoke timeout")),o+5e3),g=R.request(r,{method:"POST",headers:{"content-type":"application/json","content-length":Buffer.byteLength(i),...s?{authorization:`Bearer ${s}`}:{}}},h=>{const T=[];h.on("data",f=>T.push(f)),h.on("end",()=>{clearTimeout(N);const f=Buffer.concat(T).toString("utf8");try{const y=JSON.parse(f);y.ok?l(y.data??null):a(new Error(y.error??"invoke failed"))}catch{a(new Error(`invalid response: ${f.slice(0,200)}`))}})});g.on("error",h=>{clearTimeout(N),a(h)}),g.write(i),g.end()})}const p=new A({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 tell them 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 and the one the connector quotes. Your plain text reaches the user too \u2014 it is not private scratch space, so it is a real channel for progress and for anything the user needs mid-turn; you just need not restate the full conclusion as plain text once it is in the reply. 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).","Make the reply tool call the last thing you do in a turn and put your whole conclusion inside it: once you have called reply, any further text you write in the same turn is dropped, not delivered. Progress notes go before the reply; the reply ends the turn.",'Never explain the delivery mechanics to the user or tell them you "already sent it via the reply tool" \u2014 that is internal plumbing. If the user says they did not receive or cannot read something, do not insist you already sent it; assume the delivery or the format failed and send it again, switching format when that is the likely cause (e.g. render a diagram to an image, strip code fences, upload a file).',"If you intentionally do not want to send any visible message, call the complete tool with event_id and a final status."].join(" ")});p.setRequestHandler(C,async()=>({tools:L.map(e=>({name:e.name,description:e.description,inputSchema:e.inputSchema}))})),p.setRequestHandler($,async e=>{const t=String(e.params.name??""),o=e.params.arguments??{},n=k(t,o),r=n.name,s=n.args;if(!b.has(t))return{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0};try{if(M.has(r)){const a=await m("event_tool_call",{tool_name:r,arguments:s},3e4,0);return{content:[{type:"text",text:typeof a=="string"?a:JSON.stringify(a,null,2)}]}}const i=P(r,s),l=await m(i.action,i.params,i.timeoutMs??15e3);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=[],Y=100,H="notifications/claude/channel";function v(e){const t=e.meta;return(t&&typeof t.event_id=="string"?t.event_id:"")||"(none)"}function w(e){c.stderr.write(`[grix-stdio] ${e}
|
|
5
|
+
`),m("mcp_stdio_log",{message:e},5e3,0).catch(()=>{})}async function S(e,t){const o=e===H;if(!_){d.push({method:e,params:t}),d.length>Y&&d.shift(),o&&w(`channel notification queued before MCP connect: event=${v(t)} pending=${d.length}`);return}try{await p.notification({method:e,params:t}),o&&w(`channel notification sent to Claude: event=${v(t)}`)}catch(n){c.stderr.write(`DISPATCH_ERROR:${e}:${n}
|
|
6
|
+
`),o&&w(`channel notification failed: event=${v(t)} err=${n instanceof Error?n.message:String(n)}`)}}async function U(){for(;_&&d.length>0;){const e=d.shift();await S(e.method,e.params)}}function J(e){if(e<=0)return;const t=I.createServer(o=>{let n="";o.setEncoding("utf8"),o.on("data",r=>{n+=r;const s=n.split(`
|
|
7
|
+
`);n=s.pop()??"";for(const i of s){const l=i.trim();if(l)try{const a=JSON.parse(l);a.method&&S(a.method,a.params??{}).catch(()=>{})}catch{}}}),o.on("error",()=>{})});t.listen(e,"127.0.0.1",()=>{c.stderr.write(`NOTIFY_READY:${e}
|
|
8
|
+
`)}),t.on("error",o=>{c.stderr.write(`NOTIFY_ERROR: ${o.message}
|
|
9
|
+
`)})}async function X(){J(q);const e=new x;await p.connect(e),_=!0,c.stderr.write(`[MAIN] MCP connected, ready to dispatch notifications
|
|
10
|
+
`),await U()}X().catch(e=>{c.stderr.write(`FATAL: ${e}
|
|
10
11
|
`),c.exit(1)});
|
|
@@ -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};
|
package/openclaw-plugin/index.js
CHANGED
|
@@ -10732,7 +10732,7 @@ var TOOLS = [
|
|
|
10732
10732
|
properties: {
|
|
10733
10733
|
agent_id: { type: "string", description: "Target agent's numeric ID, passed as a string." },
|
|
10734
10734
|
agent_name: { type: "string", description: "New display name (max 100 characters, must be unique among the owner's agents)." },
|
|
10735
|
-
introduction: { type: "string", description: "New text introduction (max
|
|
10735
|
+
introduction: { type: "string", description: "New text introduction (max 3072 characters)." }
|
|
10736
10736
|
},
|
|
10737
10737
|
required: ["agent_id"]
|
|
10738
10738
|
},
|
|
@@ -11455,7 +11455,9 @@ var PHASE1_INVOKE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
11455
11455
|
"webhook_list",
|
|
11456
11456
|
"webhook_delete",
|
|
11457
11457
|
"skill_set",
|
|
11458
|
-
"skill_get"
|
|
11458
|
+
"skill_get",
|
|
11459
|
+
"egg_search",
|
|
11460
|
+
"egg_get"
|
|
11459
11461
|
]);
|
|
11460
11462
|
var ACCESS_CONTROL_ACTION_MAP = {
|
|
11461
11463
|
pair_approve: "pair_approve",
|
|
@@ -256,7 +256,7 @@ Provide at least one of `agent_name` / `introduction`.
|
|
|
256
256
|
- `agent_id` (required) — target agent's numeric ID, as a string.
|
|
257
257
|
- `agent_name` (optional) — new display name (max 100 chars); must be unique
|
|
258
258
|
among the owner's agents.
|
|
259
|
-
- `introduction` (optional) — new introduction text (max
|
|
259
|
+
- `introduction` (optional) — new introduction text (max 3072 chars).
|
|
260
260
|
|
|
261
261
|
Renaming only changes the platform-side display name; the local OpenClaw
|
|
262
262
|
channel/account configuration is untouched.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grix-connector",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.1",
|
|
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",
|
|
Binary file
|