grix-connector 2.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- import{EventEmitter as k}from"node:events";import{randomUUID as p}from"node:crypto";import _ from"node:os";import m from"ws";import{log as c}from"../log/index.js";import{detectTailnetIPv4 as v,ensureServerAndGetPort as $}from"../files/file-serve.js";function b(g){return g.replace(/(?<=[\[:,\[]\s*)(\d{16,})(?=\s*[,}\]\n])/g,'"$1"')}function w(g){const e=[...g??["stream_chunk","local_action_v1","agent_invoke"]];return e.includes("agent_invoke")||e.push("agent_invoke"),e.includes("event_result_ack")||e.push("event_result_ack"),e}const q="aibot-agent-api-v1",E=1;class l extends k{static DROPPABLE_COMMANDS=new Set(["update_binding_card"]);static BUFFER_OVERFLOW_RETAIN_COMMANDS=new Set(["event_result","codex_event","client_stream_chunk"]);static MAX_OUTBOUND_BUFFER_SIZE=1e3;static BACKPRESSURE_THRESHOLD=64*1024;ws=null;seq=0;heartbeatTimer=null;heartbeatSec=30;connected=!1;reconnecting=!1;reconnectAttempts=0;everConnected=!1;config;packetLog;pendingInvokes=new Map;seqEventMap=new Map;pendingRequests=new Map;outboundBuffer=[];ackPolicy=null;constructor(e,t){super(),this.packetLog=t?.packetLog??null,this.config={url:e.url,agentId:e.agentId,apiKey:e.apiKey,clientType:e.clientType,clientVersion:e.clientVersion??"",adapterHint:e.adapterHint??"",capabilities:w(e.capabilities),localActions:e.localActions??["exec_approve","exec_reject"],skills:e.skills}}get isConnected(){return this.connected}async connect(){let e,t;const n=(async()=>{try{if(e=await v(),e!==void 0)try{t=await $(e)}catch(r){c.warn("aibot",`file server pre-start failed: ${r}`)}}catch(r){c.warn("aibot",`tailnet detect failed: ${r}`)}})();return new Promise((r,i)=>{const u=new m(this.config.url);this.ws=u;const a=setTimeout(()=>{i(new Error("Auth timeout: no auth_ack received within 15s")),this.cleanupSocket()},15e3),o=++this.seq,d=setTimeout(()=>{this.pendingRequests.delete(o),i(new Error("Auth request timeout")),this.cleanupSocket()},15e3);this.pendingRequests.set(o,{expected:["auth_ack"],resolve:s=>{clearTimeout(a);const h=s.payload;h.code===0?(this.connected=!0,this.everConnected=!0,this.reconnectAttempts=0,h.heartbeat_sec&&(this.heartbeatSec=h.heartbeat_sec),h.ack_policy&&(this.ackPolicy=h.ack_policy,c.info("aibot",`ack_policy received: push_ack_timeout_ms=${h.ack_policy.push_ack_timeout_ms??"default"} max_retries=${h.ack_policy.max_retries??"default"} timeout_action=${h.ack_policy.timeout_action??"default"}`)),this.startHeartbeat(),this.flushOutboundBuffer(),this.emit("auth",h),r(h)):i(new Error(`Auth failed: code=${h.code} msg=${h.msg}`))},reject:s=>{clearTimeout(a),i(s)},timer:d}),u.on("open",async()=>{await n;const s={agent_id:this.config.agentId,api_key:this.config.apiKey,client_type:this.config.clientType,protocol_version:q,contract_version:E,capabilities:this.config.capabilities??[],local_actions:this.config.localActions,skills:this.config.skills};this.config.clientVersion&&(s.client="grix-connector",s.client_version=this.config.clientVersion,s.host_type=this.config.clientType,s.host_version=this.config.clientVersion),this.config.adapterHint&&(s.adapter_hint=this.config.adapterHint),s.host_meta={hostname:_.hostname(),platform:_.platform(),arch:_.arch(),os_release:_.release(),...e!==void 0&&{tailnet_ip:e},...t!==void 0&&t>0&&{file_server_port:t}},this.config.concurrency&&(s.concurrency=this.config.concurrency),this.sendPacket("auth",s,o)}),u.on("message",s=>{let h;try{h=JSON.parse(b(s.toString()))}catch{return}try{this.handlePacket(h)}catch(f){this.emitClientError(new Error(`handlePacket error: ${f}`))}}),u.on("close",(s,h)=>{this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("websocket closed"),this.emit("close",s,h.toString());const f=s!==1e3&&this.everConnected;c.info("aibot",`ws closed agent=${this.config.clientType}:${this.config.agentId} code=${s} reason=${h.toString()||"<none>"} everConnected=${this.everConnected} reconnecting=${this.reconnecting} willReconnect=${f}`),f&&this.attemptReconnect()}),u.on("error",s=>{this.emitClientError(s instanceof Error?s:new Error(String(s))),this.connected||i(s)})})}handlePacket(e){if(this.packetLog?.logInboundPacket(e.cmd,e.seq,e.payload),e.seq>0&&this.pendingRequests.has(e.seq)){const t=this.pendingRequests.get(e.seq);this.pendingRequests.delete(e.seq),clearTimeout(t.timer),t.expected.includes(e.cmd)?t.resolve(e):t.reject(new Error(`unexpected response: got ${e.cmd}, expected ${t.expected.join("/")}`));return}switch(e.cmd){case"auth_ack":break;case"ping":{this.sendPacket("pong",e.payload??{});break}case"event_msg":{this.emit("event",e.payload);break}case"local_action":{this.emit("localAction",e.payload);break}case"event_stop":{this.emit("stop",e.payload);break}case"event_revoke":{this.emit("revoke",e.payload);break}case"event_edit":{this.emit("edit",e.payload);break}case"event_cancel":{this.emit("eventCancel",e.payload);break}case"queue_clear":{this.emit("queueClear",e.payload);break}case"queue_snapshot_query":{this.emit("queueSnapshotQuery",e.payload);break}case"kicked":{if(this.emit("kicked",e.payload),this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("kicked"),this.outboundBuffer.length=0,this.reconnectAttempts=Math.max(this.reconnectAttempts,3),this.ws){try{this.ws.close(4001,"kicked")}catch{}this.ws=null}break}case"error":{const t=e.payload,n=[t.ref_cmd?`ref_cmd=${t.ref_cmd}`:"",t.ref_id?`ref_id=${t.ref_id}`:""].filter(Boolean).join(" ");this.emitClientError(new Error(`Server error: code=${t.code} msg=${t.msg}${n?` ${n}`:""}`));break}case"agent_invoke_result":{this.handleInvokeResult(e.payload);break}case"mcp_frame":{const t=e.payload;this.emit("mcpFrame",t.session_id??"",t.frame??null);break}case"send_ack":break;case"send_nack":{const t=e.payload;if(t.code===4003&&e.seq>0){const n=this.seqEventMap.get(e.seq);n&&(this.seqEventMap.delete(e.seq),this.purgeBufferedStreamChunks(n),c.warn("aibot",`stream chunk rejected (4003), purging buffered chunks for event=${n}`),this.emit("streamRejected",n,t.code))}break}case"local_action_ack":break;default:break}}sendEventAck(e){this.sendPacket("event_ack",e)||c.warn("aibot",`event_ack NOT sent (ws not open) event=${e.event_id} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}sendStreamChunk(e){!e.delta_content&&!e.is_finish&&(c.warn("aibot",`stream_chunk delta_content empty, patched to newline event=${e.event_id??""} session=${e.session_id} chunk_seq=${e.chunk_seq} is_finish=${e.is_finish}`),e={...e,delta_content:`
2
- `}),this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)||c.warn("aibot",`send_msg NOT sent (ws not open) event=${e.event_id??""} session=${e.session_id??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}editMsg(e){this.sendPacket("edit_msg",e)}sendEventResult(e){if(!this.ws||this.ws.readyState!==m.OPEN){this.sendPacket("event_result",e);return}this.sendEventResultReliable(e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendSessionActivitySet(e){this.sendPacket("session_activity_set",e)}sendCodexEvent(e){this.sendPacket("codex_event",e)}sendUpdateBindingCard(e){this.sendPacket("update_binding_card",e)}sendSkillsUpdate(e){this.sendPacket("agent_skills_update",e)}sendPing(){this.sendPacket("ping",{})}sendEventState(e){this.sendPacket("event_state",e)}sendEventCancelResult(e){this.sendPacket("event_cancel_result",e)}sendQueueClearResult(e){this.sendPacket("queue_clear_result",e)}sendQueueSnapshot(e){this.sendPacket("queue_snapshot",e)}agentInvoke(e,t,n=15e3){return new Promise((r,i)=>{const u=p(),a=Math.max(1e3,Math.min(n,6e4)),o=setTimeout(()=>{this.pendingInvokes.delete(u),i(new Error(`agent_invoke timeout: ${e}`))},a);this.pendingInvokes.set(u,{resolve:r,reject:i,timer:o}),this.sendPacket("agent_invoke",{invoke_id:u,action:e,params:t,timeout_ms:a})})}sendMcpFrame(e,t){this.sendPacket("mcp_frame",{session_id:e,frame:t})}request(e,t,n){return new Promise((r,i)=>{const u=++this.seq,a=setTimeout(()=>{this.pendingRequests.delete(u),i(new Error(`request timeout: ${e} (expected ${n.expected.join("/")})`))},n.timeoutMs);this.pendingRequests.set(u,{expected:n.expected,resolve:r,reject:i,timer:a}),this.sendPacket(e,t,u)||(this.pendingRequests.delete(u),clearTimeout(a),i(new Error(`send failed: ${e}`)))})}async sendStreamChunkRequest(e,t=2e4){return this.request("client_stream_chunk",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendText(e,t=2e4){return this.request("send_msg",{msg_type:1,...e},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendMedia(e,t=2e4){return this.request("send_msg",{...e,msg_type:2},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async editMessage(e,t=2e4){return this.request("edit_msg",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async deleteMessage(e,t,n=2e4){return this.request("delete_msg",{session_id:e,msg_id:t},{expected:["send_ack","send_nack","error"],timeoutMs:n})}async sendEventResultRequest(e,t=5e3){return this.request("event_result",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}disconnect(){c.info("aibot",`disconnect() agent=${this.config.clientType}:${this.config.agentId} wasConnected=${this.connected} reconnecting=${this.reconnecting} reconnectAttempts=${this.reconnectAttempts}`),this.connected=!1,this.everConnected=!1,this.reconnecting=!1,this.reconnectAttempts=0,this.stopHeartbeat(),this.rejectAllPendingInvokes("disconnect"),this.rejectAllPendingRequests("disconnect"),this.outboundBuffer.length=0,this.ws&&(this.ws.close(1e3,"client disconnect"),this.ws=null)}async attemptReconnect(){if(!this.reconnecting)for(this.reconnecting=!0,c.info("aibot",`attemptReconnect start agent=${this.config.clientType}:${this.config.agentId} fromAttempts=${this.reconnectAttempts}`),this.emit("disconnected");this.reconnecting;){const e=Math.min(1e3*2**this.reconnectAttempts,3e4),t=Math.floor(e*.2*Math.random());if(this.reconnectAttempts++,await new Promise(n=>setTimeout(n,e+t)),!this.reconnecting)return;try{const n=await this.connect(),r=this.reconnectAttempts;this.reconnectAttempts=0,this.reconnecting=!1,c.info("aibot",`reconnect succeeded agent=${this.config.clientType}:${this.config.agentId} attempt=${r}`),this.emit("auth",n);return}catch(n){c.warn("aibot",`reconnect failed agent=${this.config.clientType}:${this.config.agentId} attempt=${this.reconnectAttempts} err=${n instanceof Error?n.message:n}`)}}}sendPacket(e,t,n){if(this.ws&&this.ws.readyState===m.OPEN){const r=this.ws.bufferedAmount>l.BACKPRESSURE_THRESHOLD;if(!r||!l.DROPPABLE_COMMANDS.has(e)){if(r&&l.DROPPABLE_COMMANDS.has(e))return!1;const i=n??++this.seq;if(e==="client_stream_chunk"&&t&&typeof t=="object"){const a=t.event_id;if(a&&(this.seqEventMap.set(i,a),this.seqEventMap.size>200)){const o=this.seqEventMap.keys().next().value;o!==void 0&&this.seqEventMap.delete(o)}}const u={cmd:e,seq:i,payload:t};this.packetLog?.logOutboundPacket(e,i,t,"sent");try{const a=this.ws.readyState,o=this.ws.bufferedAmount;return this.ws.send(JSON.stringify(u),d=>{if(e==="event_result"){const s=t;d?c.warn("aibot",`event_result ws send callback failed event=${s.event_id??""} status=${s.status??""} seq=${i} readyState=${a} bufferedAmount=${o} err=${d.message}`):c.info("aibot",`event_result ws send callback ok event=${s.event_id??""} status=${s.status??""} seq=${i} readyState=${a} bufferedAmount=${o}`)}else if(e==="client_stream_chunk"){const s=t;d?c.warn("aibot",`stream_chunk ws send failed event=${s.event_id??""} session=${s.session_id??""} seq=${i} chunk_seq=${s.chunk_seq??""} is_finish=${s.is_finish??""} readyState=${a} bufferedAmount=${o} err=${d.message}`):c.info("aibot",`stream_chunk ws send ok event=${s.event_id??""} session=${s.session_id??""} seq=${i} chunk_seq=${s.chunk_seq??""} is_finish=${s.is_finish??""} readyState=${a} bufferedAmount=${o}`)}else if(e==="event_ack"){const s=t;d?c.warn("aibot",`event_ack ws send failed event=${s.event_id??""} seq=${i} readyState=${a} bufferedAmount=${o} err=${d.message}`):c.info("aibot",`event_ack ws send ok event=${s.event_id??""} seq=${i} readyState=${a} bufferedAmount=${o}`)}else if(e==="send_msg"){const s=t;d?c.warn("aibot",`send_msg ws send failed event=${s.event_id??""} session=${s.session_id??""} seq=${i} readyState=${a} bufferedAmount=${o} err=${d.message}`):c.info("aibot",`send_msg ws send ok event=${s.event_id??""} session=${s.session_id??""} seq=${i} readyState=${a} bufferedAmount=${o}`)}else if(d){const s=t;c.warn("aibot",`${e} ws send failed seq=${i} session=${s.session_id??""} event=${s.event_id??""} client_msg_id=${s.client_msg_id??""} readyState=${a} bufferedAmount=${o} err=${d.message}`)}}),!0}catch(a){return this.emitClientError(new Error(`sendPacket failed: ${a}`)),!1}}}if(l.DROPPABLE_COMMANDS.has(e))return this.packetLog?.logOutboundPacket(e,n??0,t,"dropped"),!1;if(n!==void 0)return this.packetLog?.logOutboundPacket(e,n,t,"dropped"),!1;if(this.outboundBuffer.length>=l.MAX_OUTBOUND_BUFFER_SIZE&&(this.outboundBuffer=this.outboundBuffer.filter(r=>l.BUFFER_OVERFLOW_RETAIN_COMMANDS.has(r.cmd)),this.outboundBuffer.length>=l.MAX_OUTBOUND_BUFFER_SIZE&&this.outboundBuffer.shift()),this.outboundBuffer.push({cmd:e,payload:t}),this.packetLog?.logOutboundPacket(e,n??0,t,"buffered"),e==="client_stream_chunk"){const r=t;c.info("aibot",`stream_chunk buffered (ws not open) event=${r.event_id??""} session=${r.session_id??""} chunk_seq=${r.chunk_seq??""} is_finish=${r.is_finish??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}return!1}async sendEventResultReliable(e){const t=this.ackPolicy?.max_retries??3,n=this.ackPolicy?.push_ack_timeout_ms??5e3,r=750;for(let i=1;i<=t;i++){const u=this.ws?.readyState??-1,a=this.ws?.bufferedAmount??0;c.info("aibot",`event_result send attempt event=${e.event_id} status=${e.status} attempt=${i}/${t} readyState=${u} bufferedAmount=${a}`);try{const o=await this.sendEventResultRequest(e,n);if(o.cmd==="send_ack"){const s=o.payload;c.info("aibot",`event_result ack event=${e.event_id} status=${e.status} attempt=${i}/${t} ack_event=${s.event_id??""} ack_status=${s.status??""}`);return}const d=o.payload;if(c.warn("aibot",`event_result rejected event=${e.event_id} status=${e.status} attempt=${i}/${t} cmd=${o.cmd} code=${d.code??""} msg=${d.msg??""}${d.ref_cmd?` ref_cmd=${d.ref_cmd}`:""}${d.ref_id?` ref_id=${d.ref_id}`:""}`),d.code===4003){c.warn("aibot",`event_result stopping retries: 4003 ownership denied event=${e.event_id}`);return}return}catch(o){const d=o instanceof Error?o.message:String(o);if(c.warn("aibot",`event_result attempt failed event=${e.event_id} status=${e.status} attempt=${i}/${t} err=${d}`),i===t){this.emitClientError(new Error(`event_result ack failed after ${t} attempts: event=${e.event_id} status=${e.status}`));return}await new Promise(s=>setTimeout(s,r*i))}}}purgeBufferedStreamChunks(e){const t=this.outboundBuffer.length;this.outboundBuffer=this.outboundBuffer.filter(n=>n.cmd!=="client_stream_chunk"?!0:n.payload?.event_id!==e),this.outboundBuffer.length<t&&c.info("aibot",`purged ${t-this.outboundBuffer.length} buffered stream chunks for event=${e}`)}emitClientError(e){if(this.listenerCount("error")===0){c.warn("aibot",`Client error (no listeners): ${e.message}`);return}this.emit("error",e)}flushOutboundBuffer(){if(this.outboundBuffer.length===0||!this.ws||this.ws.readyState!==m.OPEN)return;const e=this.outboundBuffer;this.outboundBuffer=[];for(const{cmd:t,payload:n}of e){const r=++this.seq;if(t==="client_stream_chunk"&&n&&typeof n=="object"){const u=n.event_id;u&&this.seqEventMap.set(r,u)}const i={cmd:t,seq:r,payload:n};try{this.ws.send(JSON.stringify(i))}catch{break}}if(this.seqEventMap.size>200){const t=[...this.seqEventMap.entries()].sort((n,r)=>n[0]-r[0]);this.seqEventMap.clear();for(const[n,r]of t.slice(-100))this.seqEventMap.set(n,r)}}handleInvokeResult(e){const t=this.pendingInvokes.get(e.invoke_id);t&&(this.pendingInvokes.delete(e.invoke_id),clearTimeout(t.timer),e.code===0?t.resolve(e.data??null):t.reject(new Error(`agent_invoke error code=${e.code}: ${e.msg??""}`)))}rejectAllPendingInvokes(e){for(const[,t]of this.pendingInvokes)clearTimeout(t.timer),t.reject(new Error(`agent_invoke canceled: ${e}`));this.pendingInvokes.clear()}rejectAllPendingRequests(e){for(const[,t]of this.pendingRequests)clearTimeout(t.timer),t.reject(new Error(`request canceled: ${e}`));this.pendingRequests.clear()}cleanupSocket(){if(this.ws){try{this.ws.close()}catch{}this.ws=null}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.connected&&this.request("ping",{ts:Date.now()},{expected:["pong"],timeoutMs:5e3}).catch(()=>{this.connected&&(this.cleanupSocket(),this.attemptReconnect())})},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{l as AibotClient};
1
+ import{EventEmitter as k}from"node:events";import{randomUUID as v}from"node:crypto";import p from"node:os";import m from"ws";import{log as r}from"../log/index.js";import{getMachineName as $}from"../util/index.js";import{detectTailnetIPv4 as b,ensureServerAndGetPort as w,getFileServerHttpsPort as q}from"../files/file-serve.js";function E(g){return g.replace(/(?<=[\[:,\[]\s*)(\d{16,})(?=\s*[,}\]\n])/g,'"$1"')}function P(g){const e=[...g??["stream_chunk","local_action_v1","agent_invoke"]];return e.includes("agent_invoke")||e.push("agent_invoke"),e.includes("event_result_ack")||e.push("event_result_ack"),e}const S="aibot-agent-api-v1",y=1;class f extends k{static DROPPABLE_COMMANDS=new Set(["update_binding_card"]);static BUFFER_OVERFLOW_RETAIN_COMMANDS=new Set(["event_result","codex_event","client_stream_chunk"]);static MAX_OUTBOUND_BUFFER_SIZE=1e3;static BACKPRESSURE_THRESHOLD=64*1024;ws=null;seq=0;heartbeatTimer=null;heartbeatSec=30;connected=!1;reconnecting=!1;reconnectAttempts=0;everConnected=!1;config;packetLog;pendingInvokes=new Map;seqEventMap=new Map;pendingRequests=new Map;outboundBuffer=[];ackPolicy=null;constructor(e,t){super(),this.packetLog=t?.packetLog??null,this.config={url:e.url,agentId:e.agentId,apiKey:e.apiKey,clientType:e.clientType,clientVersion:e.clientVersion??"",adapterHint:e.adapterHint??"",capabilities:P(e.capabilities),localActions:e.localActions??["exec_approve","exec_reject"],skills:e.skills}}get isConnected(){return this.connected}async connect(){let e,t,s;const o=(async()=>{try{if(e=await b(),e!==void 0)try{t=await w(e);const n=q();n>0&&(s=n)}catch(n){r.warn("aibot",`file server pre-start failed: ${n}`)}}catch(n){r.warn("aibot",`tailnet detect failed: ${n}`)}})();return new Promise((n,h)=>{const c=new m(this.config.url);this.ws=c;const a=setTimeout(()=>{h(new Error("Auth timeout: no auth_ack received within 15s")),this.cleanupSocket()},15e3),d=++this.seq,i=setTimeout(()=>{this.pendingRequests.delete(d),h(new Error("Auth request timeout")),this.cleanupSocket()},15e3);this.pendingRequests.set(d,{expected:["auth_ack"],resolve:u=>{clearTimeout(a);const l=u.payload;l.code===0?(this.connected=!0,this.everConnected=!0,this.reconnectAttempts=0,l.heartbeat_sec&&(this.heartbeatSec=l.heartbeat_sec),l.ack_policy&&(this.ackPolicy=l.ack_policy,r.info("aibot",`ack_policy received: push_ack_timeout_ms=${l.ack_policy.push_ack_timeout_ms??"default"} max_retries=${l.ack_policy.max_retries??"default"} timeout_action=${l.ack_policy.timeout_action??"default"}`)),this.startHeartbeat(),this.flushOutboundBuffer(),this.emit("auth",l),n(l)):h(new Error(`Auth failed: code=${l.code} msg=${l.msg}`))},reject:u=>{clearTimeout(a),h(u)},timer:i}),c.on("open",async()=>{await o;const u={agent_id:this.config.agentId,api_key:this.config.apiKey,client_type:this.config.clientType,protocol_version:S,contract_version:y,capabilities:this.config.capabilities??[],local_actions:this.config.localActions,skills:this.config.skills};this.config.clientVersion&&(u.client="grix-connector",u.client_version=this.config.clientVersion,u.host_type=this.config.clientType,u.host_version=this.config.clientVersion),this.config.adapterHint&&(u.adapter_hint=this.config.adapterHint),u.host_meta={hostname:$(),platform:p.platform(),arch:p.arch(),os_release:p.release(),...e!==void 0&&{tailnet_ip:e},...t!==void 0&&t>0&&{file_server_port:t},...s!==void 0&&s>0&&{file_server_https_port:s}},this.config.concurrency&&(u.concurrency=this.config.concurrency),this.sendPacket("auth",u,d)}),c.on("message",u=>{let l;try{l=JSON.parse(E(u.toString()))}catch{return}try{this.handlePacket(l)}catch(_){this.emitClientError(new Error(`handlePacket error: ${_}`))}}),c.on("close",(u,l)=>{this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("websocket closed"),this.emit("close",u,l.toString());const _=u!==1e3&&this.everConnected;r.info("aibot",`ws closed agent=${this.config.clientType}:${this.config.agentId} code=${u} reason=${l.toString()||"<none>"} everConnected=${this.everConnected} reconnecting=${this.reconnecting} willReconnect=${_}`),_&&this.attemptReconnect()}),c.on("error",u=>{this.emitClientError(u instanceof Error?u:new Error(String(u))),this.connected||h(u)})})}handlePacket(e){if(this.packetLog?.logInboundPacket(e.cmd,e.seq,e.payload),e.seq>0&&this.pendingRequests.has(e.seq)){const t=this.pendingRequests.get(e.seq);this.pendingRequests.delete(e.seq),clearTimeout(t.timer),t.expected.includes(e.cmd)?t.resolve(e):t.reject(new Error(`unexpected response: got ${e.cmd}, expected ${t.expected.join("/")}`));return}switch(e.cmd){case"auth_ack":break;case"ping":{this.sendPacket("pong",e.payload??{});break}case"event_msg":{this.emit("event",e.payload);break}case"local_action":{this.emit("localAction",e.payload);break}case"event_stop":{this.emit("stop",e.payload);break}case"event_revoke":{this.emit("revoke",e.payload);break}case"event_edit":{this.emit("edit",e.payload);break}case"event_cancel":{this.emit("eventCancel",e.payload);break}case"queue_clear":{this.emit("queueClear",e.payload);break}case"queue_snapshot_query":{this.emit("queueSnapshotQuery",e.payload);break}case"kicked":{if(this.emit("kicked",e.payload),this.connected=!1,this.stopHeartbeat(),this.rejectAllPendingRequests("kicked"),this.outboundBuffer.length=0,this.reconnectAttempts=Math.max(this.reconnectAttempts,3),this.ws){try{this.ws.close(4001,"kicked")}catch{}this.ws=null}break}case"error":{const t=e.payload,s=[t.ref_cmd?`ref_cmd=${t.ref_cmd}`:"",t.ref_id?`ref_id=${t.ref_id}`:""].filter(Boolean).join(" ");this.emitClientError(new Error(`Server error: code=${t.code} msg=${t.msg}${s?` ${s}`:""}`));break}case"agent_invoke_result":{this.handleInvokeResult(e.payload);break}case"mcp_frame":{const t=e.payload;this.emit("mcpFrame",t.session_id??"",t.frame??null);break}case"send_ack":break;case"send_nack":{const t=e.payload;if(t.code===4003&&e.seq>0){const s=this.seqEventMap.get(e.seq);s&&(this.seqEventMap.delete(e.seq),this.purgeBufferedStreamChunks(s),r.warn("aibot",`stream chunk rejected (4003), purging buffered chunks for event=${s}`),this.emit("streamRejected",s,t.code))}break}case"local_action_ack":break;default:break}}sendEventAck(e){this.sendPacket("event_ack",e)||r.warn("aibot",`event_ack NOT sent (ws not open) event=${e.event_id} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}sendStreamChunk(e){!e.delta_content&&!e.is_finish&&(r.warn("aibot",`stream_chunk delta_content empty, patched to newline event=${e.event_id??""} session=${e.session_id} chunk_seq=${e.chunk_seq} is_finish=${e.is_finish}`),e={...e,delta_content:`
2
+ `}),this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)||r.warn("aibot",`send_msg NOT sent (ws not open) event=${e.event_id??""} session=${e.session_id??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}editMsg(e){this.sendPacket("edit_msg",e)}sendEventResult(e){if(!this.ws||this.ws.readyState!==m.OPEN){this.sendPacket("event_result",e);return}this.sendEventResultReliable(e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendSessionActivitySet(e){this.sendPacket("session_activity_set",e)}sendCodexEvent(e){this.sendPacket("codex_event",e)}sendUpdateBindingCard(e){this.sendPacket("update_binding_card",e)}sendSkillsUpdate(e){this.sendPacket("agent_skills_update",e)}sendPing(){this.sendPacket("ping",{})}sendEventState(e){this.sendPacket("event_state",e)}sendEventCancelResult(e){this.sendPacket("event_cancel_result",e)}sendQueueClearResult(e){this.sendPacket("queue_clear_result",e)}sendQueueSnapshot(e){this.sendPacket("queue_snapshot",e)}agentInvoke(e,t,s=15e3){return new Promise((o,n)=>{const h=v(),c=Math.max(1e3,Math.min(s,6e4)),a=setTimeout(()=>{this.pendingInvokes.delete(h),n(new Error(`agent_invoke timeout: ${e}`))},c);this.pendingInvokes.set(h,{resolve:o,reject:n,timer:a}),this.sendPacket("agent_invoke",{invoke_id:h,action:e,params:t,timeout_ms:c})})}sendMcpFrame(e,t){this.sendPacket("mcp_frame",{session_id:e,frame:t})}request(e,t,s){return new Promise((o,n)=>{const h=++this.seq,c=setTimeout(()=>{this.pendingRequests.delete(h),n(new Error(`request timeout: ${e} (expected ${s.expected.join("/")})`))},s.timeoutMs);this.pendingRequests.set(h,{expected:s.expected,resolve:o,reject:n,timer:c}),this.sendPacket(e,t,h)||(this.pendingRequests.delete(h),clearTimeout(c),n(new Error(`send failed: ${e}`)))})}async sendStreamChunkRequest(e,t=2e4){return this.request("client_stream_chunk",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendText(e,t=2e4){return this.request("send_msg",{msg_type:1,...e},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async sendMedia(e,t=2e4){return this.request("send_msg",{...e,msg_type:2},{expected:["send_ack","send_nack","error"],timeoutMs:t})}async editMessage(e,t=2e4){return this.request("edit_msg",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}async deleteMessage(e,t,s=2e4){return this.request("delete_msg",{session_id:e,msg_id:t},{expected:["send_ack","send_nack","error"],timeoutMs:s})}async sendEventResultRequest(e,t=5e3){return this.request("event_result",e,{expected:["send_ack","send_nack","error"],timeoutMs:t})}disconnect(){r.info("aibot",`disconnect() agent=${this.config.clientType}:${this.config.agentId} wasConnected=${this.connected} reconnecting=${this.reconnecting} reconnectAttempts=${this.reconnectAttempts}`),this.connected=!1,this.everConnected=!1,this.reconnecting=!1,this.reconnectAttempts=0,this.stopHeartbeat(),this.rejectAllPendingInvokes("disconnect"),this.rejectAllPendingRequests("disconnect"),this.outboundBuffer.length=0,this.ws&&(this.ws.close(1e3,"client disconnect"),this.ws=null)}async attemptReconnect(){if(!this.reconnecting)for(this.reconnecting=!0,r.info("aibot",`attemptReconnect start agent=${this.config.clientType}:${this.config.agentId} fromAttempts=${this.reconnectAttempts}`),this.emit("disconnected");this.reconnecting;){const e=Math.min(1e3*2**this.reconnectAttempts,3e4),t=Math.floor(e*.2*Math.random());if(this.reconnectAttempts++,await new Promise(s=>setTimeout(s,e+t)),!this.reconnecting)return;try{const s=await this.connect(),o=this.reconnectAttempts;this.reconnectAttempts=0,this.reconnecting=!1,r.info("aibot",`reconnect succeeded agent=${this.config.clientType}:${this.config.agentId} attempt=${o}`),this.emit("auth",s);return}catch(s){r.warn("aibot",`reconnect failed agent=${this.config.clientType}:${this.config.agentId} attempt=${this.reconnectAttempts} err=${s instanceof Error?s.message:s}`)}}}sendPacket(e,t,s){if(this.ws&&this.ws.readyState===m.OPEN){const o=this.ws.bufferedAmount>f.BACKPRESSURE_THRESHOLD;if(!o||!f.DROPPABLE_COMMANDS.has(e)){if(o&&f.DROPPABLE_COMMANDS.has(e))return!1;const n=s??++this.seq;if(e==="client_stream_chunk"&&t&&typeof t=="object"){const c=t.event_id;if(c&&(this.seqEventMap.set(n,c),this.seqEventMap.size>200)){const a=this.seqEventMap.keys().next().value;a!==void 0&&this.seqEventMap.delete(a)}}const h={cmd:e,seq:n,payload:t};this.packetLog?.logOutboundPacket(e,n,t,"sent");try{const c=this.ws.readyState,a=this.ws.bufferedAmount;return this.ws.send(JSON.stringify(h),d=>{if(e==="event_result"){const i=t;d?r.warn("aibot",`event_result ws send callback failed event=${i.event_id??""} status=${i.status??""} seq=${n} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`event_result ws send callback ok event=${i.event_id??""} status=${i.status??""} seq=${n} readyState=${c} bufferedAmount=${a}`)}else if(e==="client_stream_chunk"){const i=t;d?r.warn("aibot",`stream_chunk ws send failed event=${i.event_id??""} session=${i.session_id??""} seq=${n} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`stream_chunk ws send ok event=${i.event_id??""} session=${i.session_id??""} seq=${n} chunk_seq=${i.chunk_seq??""} is_finish=${i.is_finish??""} readyState=${c} bufferedAmount=${a}`)}else if(e==="event_ack"){const i=t;d?r.warn("aibot",`event_ack ws send failed event=${i.event_id??""} seq=${n} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`event_ack ws send ok event=${i.event_id??""} seq=${n} readyState=${c} bufferedAmount=${a}`)}else if(e==="send_msg"){const i=t;d?r.warn("aibot",`send_msg ws send failed event=${i.event_id??""} session=${i.session_id??""} seq=${n} readyState=${c} bufferedAmount=${a} err=${d.message}`):r.info("aibot",`send_msg ws send ok event=${i.event_id??""} session=${i.session_id??""} seq=${n} readyState=${c} bufferedAmount=${a}`)}else if(d){const i=t;r.warn("aibot",`${e} ws send failed seq=${n} session=${i.session_id??""} event=${i.event_id??""} client_msg_id=${i.client_msg_id??""} readyState=${c} bufferedAmount=${a} err=${d.message}`)}}),!0}catch(c){return this.emitClientError(new Error(`sendPacket failed: ${c}`)),!1}}}if(f.DROPPABLE_COMMANDS.has(e))return this.packetLog?.logOutboundPacket(e,s??0,t,"dropped"),!1;if(s!==void 0)return this.packetLog?.logOutboundPacket(e,s,t,"dropped"),!1;if(this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&(this.outboundBuffer=this.outboundBuffer.filter(o=>f.BUFFER_OVERFLOW_RETAIN_COMMANDS.has(o.cmd)),this.outboundBuffer.length>=f.MAX_OUTBOUND_BUFFER_SIZE&&this.outboundBuffer.shift()),this.outboundBuffer.push({cmd:e,payload:t}),this.packetLog?.logOutboundPacket(e,s??0,t,"buffered"),e==="client_stream_chunk"){const o=t;r.info("aibot",`stream_chunk buffered (ws not open) event=${o.event_id??""} session=${o.session_id??""} chunk_seq=${o.chunk_seq??""} is_finish=${o.is_finish??""} ws=${this.ws?`state=${this.ws.readyState}`:"null"}`)}return!1}async sendEventResultReliable(e){const t=this.ackPolicy?.max_retries??3,s=this.ackPolicy?.push_ack_timeout_ms??5e3,o=750;for(let n=1;n<=t;n++){const h=this.ws?.readyState??-1,c=this.ws?.bufferedAmount??0;r.info("aibot",`event_result send attempt event=${e.event_id} status=${e.status} attempt=${n}/${t} readyState=${h} bufferedAmount=${c}`);try{const a=await this.sendEventResultRequest(e,s);if(a.cmd==="send_ack"){const i=a.payload;r.info("aibot",`event_result ack event=${e.event_id} status=${e.status} attempt=${n}/${t} ack_event=${i.event_id??""} ack_status=${i.status??""}`);return}const d=a.payload;if(r.warn("aibot",`event_result rejected event=${e.event_id} status=${e.status} attempt=${n}/${t} cmd=${a.cmd} code=${d.code??""} msg=${d.msg??""}${d.ref_cmd?` ref_cmd=${d.ref_cmd}`:""}${d.ref_id?` ref_id=${d.ref_id}`:""}`),d.code===4003){r.warn("aibot",`event_result stopping retries: 4003 ownership denied event=${e.event_id}`);return}return}catch(a){const d=a instanceof Error?a.message:String(a);if(r.warn("aibot",`event_result attempt failed event=${e.event_id} status=${e.status} attempt=${n}/${t} err=${d}`),n===t){this.emitClientError(new Error(`event_result ack failed after ${t} attempts: event=${e.event_id} status=${e.status}`));return}await new Promise(i=>setTimeout(i,o*n))}}}purgeBufferedStreamChunks(e){const t=this.outboundBuffer.length;this.outboundBuffer=this.outboundBuffer.filter(s=>s.cmd!=="client_stream_chunk"?!0:s.payload?.event_id!==e),this.outboundBuffer.length<t&&r.info("aibot",`purged ${t-this.outboundBuffer.length} buffered stream chunks for event=${e}`)}emitClientError(e){if(this.listenerCount("error")===0){r.warn("aibot",`Client error (no listeners): ${e.message}`);return}this.emit("error",e)}flushOutboundBuffer(){if(this.outboundBuffer.length===0||!this.ws||this.ws.readyState!==m.OPEN)return;const e=this.outboundBuffer;this.outboundBuffer=[];for(const{cmd:t,payload:s}of e){const o=++this.seq;if(t==="client_stream_chunk"&&s&&typeof s=="object"){const h=s.event_id;h&&this.seqEventMap.set(o,h)}const n={cmd:t,seq:o,payload:s};try{this.ws.send(JSON.stringify(n))}catch{break}}if(this.seqEventMap.size>200){const t=[...this.seqEventMap.entries()].sort((s,o)=>s[0]-o[0]);this.seqEventMap.clear();for(const[s,o]of t.slice(-100))this.seqEventMap.set(s,o)}}handleInvokeResult(e){const t=this.pendingInvokes.get(e.invoke_id);t&&(this.pendingInvokes.delete(e.invoke_id),clearTimeout(t.timer),e.code===0?t.resolve(e.data??null):t.reject(new Error(`agent_invoke error code=${e.code}: ${e.msg??""}`)))}rejectAllPendingInvokes(e){for(const[,t]of this.pendingInvokes)clearTimeout(t.timer),t.reject(new Error(`agent_invoke canceled: ${e}`));this.pendingInvokes.clear()}rejectAllPendingRequests(e){for(const[,t]of this.pendingRequests)clearTimeout(t.timer),t.reject(new Error(`request canceled: ${e}`));this.pendingRequests.clear()}cleanupSocket(){if(this.ws){try{this.ws.close()}catch{}this.ws=null}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.connected&&this.request("ping",{ts:Date.now()},{expected:["pong"],timeoutMs:5e3}).catch(()=>{this.connected&&(this.cleanupSocket(),this.attemptReconnect())})},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{f as AibotClient};
@@ -1 +1 @@
1
- import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const i of c){if(!p&&i.name.startsWith("."))continue;const t=l(a,i.name),e={id:t,name:i.name,is_directory:i.isDirectory()};try{if(i.isDirectory()){const o=await m(t);e.modified_at=o.mtime.toISOString()}else{const o=await m(t);e.size=o.size,e.modified_at=o.mtime.toISOString(),e.mime_type=n(i.name)}}catch{}s.push(e)}return s.sort((i,t)=>i.is_directory!==t.is_directory?i.is_directory?-1:1:i.name.localeCompare(t.name)),s}export{f as listFiles,n as resolveMimeType};
1
+ import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const t of c){if(!p&&t.name.startsWith("."))continue;const i=l(a,t.name),e={id:i,name:t.name,is_directory:t.isDirectory()};try{if(t.isDirectory()){const o=await m(i);e.modified_at=o.mtime.toISOString()}else{const o=await m(i);e.size=o.size,e.modified_at=o.mtime.toISOString(),e.mime_type=n(t.name)}}catch{}s.push(e)}return s.sort((t,i)=>t.is_directory!==i.is_directory?t.is_directory?-1:1:t.name.localeCompare(i.name)),s}export{f as listFiles,n as resolveMimeType};
@@ -0,0 +1,40 @@
1
+ import a from"node-forge";import{createHash as P}from"node:crypto";import{mkdir as C,readFile as l,writeFile as u}from"node:fs/promises";import{join as y}from"node:path";import{hostname as m}from"node:os";import{resolveRuntimePaths as D}from"../config/paths.js";const T=10,A=365,E=30,w=30;let o=null;const s=new Map;function h(){return y(D().rootDir,"certs")}function p(){return"00"+a.util.bytesToHex(a.random.getBytesSync(16))}function v(t){const e=new Date;return e.setFullYear(e.getFullYear()+t),e}function b(t){const e=new Date;return e.setDate(e.getDate()+t),e}function d(){const t=new Date;return t.setDate(t.getDate()-1),t}function x(){const t=a.pki.rsa.generateKeyPair(2048),e=a.pki.createCertificate();e.publicKey=t.publicKey,e.serialNumber=p(),e.validity.notBefore=d(),e.validity.notAfter=v(T);const i=[{name:"commonName",value:`Grix Tailnet Local CA (${m()})`},{name:"organizationName",value:"Grix Connector"}];return e.setSubject(i),e.setIssuer(i),e.setExtensions([{name:"basicConstraints",cA:!0,critical:!0},{name:"keyUsage",keyCertSign:!0,cRLSign:!0,critical:!0},{name:"subjectKeyIdentifier"}]),e.sign(t.privateKey,a.md.sha256.create()),{certPem:a.pki.certificateToPem(e),keyPem:a.pki.privateKeyToPem(t.privateKey)}}function I(t,e){const i=a.pki.certificateFromPem(e.certPem),r=a.pki.privateKeyFromPem(e.keyPem),c=a.pki.rsa.generateKeyPair(2048),n=a.pki.createCertificate();return n.publicKey=c.publicKey,n.serialNumber=p(),n.validity.notBefore=d(),n.validity.notAfter=b(A),n.setSubject([{name:"commonName",value:t}]),n.setIssuer(i.subject.attributes),n.setExtensions([{name:"basicConstraints",cA:!1},{name:"keyUsage",digitalSignature:!0,keyEncipherment:!0,critical:!0},{name:"extKeyUsage",serverAuth:!0},{name:"subjectAltName",altNames:[{type:7,ip:t}]}]),n.sign(r,a.md.sha256.create()),{key:a.pki.privateKeyToPem(c.privateKey),cert:a.pki.certificateToPem(n)}}function g(t,e){try{const i=a.pki.certificateFromPem(t),r=new Date;return r.setDate(r.getDate()+e),i.validity.notAfter>r}catch{return!1}}async function k(){if(o)return o;const t=h(),e=y(t,"tailnet-ca-cert.pem"),i=y(t,"tailnet-ca-key.pem");try{const c=await l(e,"utf8"),n=await l(i,"utf8");if(g(c,E))return o={certPem:c,keyPem:n},o}catch{}await C(t,{recursive:!0});const r=x();return await u(i,r.keyPem,{mode:384}),await u(e,r.certPem),o=r,s.clear(),r}async function N(){return(await k()).certPem}function F(t){return t.replace(/-----BEGIN CERTIFICATE-----/g,"").replace(/-----END CERTIFICATE-----/g,"").replace(/\s+/g,"")}function f(t){const e=P("sha256").update(t).digest("hex");return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,32)].join("-").toUpperCase()}async function R(){const t=await N(),e=F(t),i=f(t+":profile"),r=f(t+":cert"),c=`Grix Tailnet Local CA (${m()})`;return`<?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>PayloadContent</key>
6
+ <array>
7
+ <dict>
8
+ <key>PayloadCertificateFileName</key>
9
+ <string>grix-tailnet-ca.crt</string>
10
+ <key>PayloadContent</key>
11
+ <data>${e}</data>
12
+ <key>PayloadDescription</key>
13
+ <string>Grix Tailnet \u672C\u673A\u81EA\u7B7E\u6839\u8BC1\u4E66</string>
14
+ <key>PayloadDisplayName</key>
15
+ <string>${c}</string>
16
+ <key>PayloadIdentifier</key>
17
+ <string>com.grix.tailnet.ca.cert</string>
18
+ <key>PayloadType</key>
19
+ <string>com.apple.security.root</string>
20
+ <key>PayloadUUID</key>
21
+ <string>${r}</string>
22
+ <key>PayloadVersion</key>
23
+ <integer>1</integer>
24
+ </dict>
25
+ </array>
26
+ <key>PayloadDescription</key>
27
+ <string>\u5B89\u88C5\u540E\u5373\u53EF\u4FE1\u4EFB\u672C\u673A Grix Tailnet \u6587\u4EF6\u670D\u52A1\u7684 HTTPS \u94FE\u63A5</string>
28
+ <key>PayloadDisplayName</key>
29
+ <string>${c}</string>
30
+ <key>PayloadIdentifier</key>
31
+ <string>com.grix.tailnet.ca</string>
32
+ <key>PayloadType</key>
33
+ <string>Configuration</string>
34
+ <key>PayloadUUID</key>
35
+ <string>${i}</string>
36
+ <key>PayloadVersion</key>
37
+ <integer>1</integer>
38
+ </dict>
39
+ </plist>
40
+ `}async function Y(t){const e=s.get(t);if(e&&g(e.cert,w))return e;const i=await k(),r=I(t,i);return s.set(t,r),r}function j(){o=null,s.clear()}export{N as getCaCertPem,R as getCaMobileConfig,Y as getLeafCredentials,j as resetCertCache};
@@ -1,2 +1,2 @@
1
- import{randomUUID as I}from"node:crypto";import{execFile as H}from"node:child_process";import N from"node:http";import{createReadStream as U,createWriteStream as D}from"node:fs";import{stat as P,rename as L,unlink as _,access as F}from"node:fs/promises";import{basename as y,extname as b,isAbsolute as A,join as M,normalize as R,resolve as O}from"node:path";import*as z from"node:os";const j=600*1e3,k={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".bmp":"image/bmp",".tiff":"image/tiff",".tif":"image/tiff",".ico":"image/x-icon",".avif":"image/avif"};function V(t){return k[b(t).toLowerCase()]}const G={".mp4":"video/mp4",".m4v":"video/mp4",".mov":"video/quicktime",".webm":"video/webm",".ogv":"video/ogg",".mkv":"video/x-matroska",".avi":"video/x-msvideo",".3gp":"video/3gpp",".ts":"video/mp2t"},W={".mp3":"audio/mpeg",".m4a":"audio/mp4",".aac":"audio/aac",".wav":"audio/wav",".ogg":"audio/ogg",".oga":"audio/ogg",".opus":"audio/opus",".flac":"audio/flac",".weba":"audio/webm"};function B(t){const e=b(t).toLowerCase();return k[e]??G[e]??W[e]}function C(t){const e=t.split(".");if(e.length!==4)return!1;const o=Number(e[0]),i=Number(e[1]);return!Number.isInteger(o)||!Number.isInteger(i)?!1:o===100&&i>=64&&i<=127}function J(){return new Promise(t=>{H("tailscale",["ip","-4"],{timeout:3e3},(e,o)=>{if(e){t(void 0);return}const i=o.trim().split(`
2
- `)[0].trim();t(i&&C(i)?i:void 0)})})}function X(){const t=z.networkInterfaces();for(const e of Object.values(t))if(e){for(const o of e)if(!(o.family!=="IPv4"||o.internal)&&C(o.address))return o.address}}async function Y(){const t=await J();return t!==void 0?t:X()}const p=new Map;let g=null,w="",v=0,h=null;function Z(t){for(const[e,o]of p)o.expiresAt<=t&&p.delete(e)}const K=2*1024*1024*1024;function Q(t){const e=t.socket.remoteAddress??"",o=e.startsWith("::ffff:")?e.slice(7):e;return C(o)}async function q(t,e){const o=b(e),i=y(e,o);let n=M(t,e),a=0;for(;;)try{await F(n),a++,n=M(t,`${i}(${a})${o}`)}catch{return n}}function ee(t,e,o){return new Promise((i,n)=>{const a=D(e);let r=0,c=!1;const d=()=>{c||(c=!0,a.destroy(),_(e).catch(()=>{}))};t.on("data",s=>{r+=s.length,r>K&&(d(),n(Object.assign(new Error("file too large"),{code:413})))}),a.on("error",s=>{d(),n(s)}),t.on("error",s=>{d(),n(s)}),a.on("finish",async()=>{if(!c){c=!0;try{await L(e,o),i()}catch(s){_(e).catch(()=>{}),n(s)}}}),t.pipe(a)})}async function te(t,e){if(!Q(t)){e.statusCode=403,e.end("forbidden");return}const i=new URL(t.url??"/",`http://${t.headers.host}`).searchParams.get("dir")??"";if(!A(i)||R(i)!==O(i)){e.statusCode=400,e.end("invalid dir");return}const n=t.headers["x-filename"]??"",a=Array.isArray(n)?n[0]:n;let r;try{r=decodeURIComponent(a)}catch{r=a}if(!r||r.includes("/")||r.includes("\\")||r==="."||r===".."){e.statusCode=400,e.end("invalid filename");return}try{if(!(await P(i)).isDirectory()){e.statusCode=400,e.end("dir not found");return}}catch{e.statusCode=400,e.end("dir not found");return}const c=await q(i,r),d=`${c}.${I()}.tmp`;try{await ee(t,d,c),e.statusCode=200,e.setHeader("Content-Type","application/json"),e.end(JSON.stringify({ok:!0,path:c,name:y(c)}))}catch(s){s.code===413?(e.statusCode=413,e.end("file too large")):(e.statusCode=500,e.end("upload failed"))}}async function ne(t){const e=N.createServer((i,n)=>{n.setHeader("Access-Control-Allow-Origin","*");const a=String(i.url??"").split("?")[0];if(a==="/ping"){n.statusCode=200,n.end("ok");return}if(a==="/upload"&&i.method==="POST"){te(i,n).catch(()=>{n.headersSent||(n.statusCode=500,n.end("internal error"))});return}const c=/^\/d\/([A-Za-z0-9-]+)$/.exec(a)?.[1],d=c?p.get(c):void 0;if(!d||d.expiresAt<=Date.now()){n.statusCode=404,n.end("not found");return}const s=B(d.fileName);n.setHeader("Content-Type",s??"application/octet-stream"),n.setHeader("Content-Disposition",`${s?"inline":"attachment"}; filename*=UTF-8''${encodeURIComponent(d.fileName)}`),n.setHeader("Accept-Ranges","bytes");const u=d.size;let l=0,m=u-1;const $=i.headers.range;if($){const f=/^bytes=(\d*)-(\d*)$/.exec($.trim());if(!f||f[1]===""&&f[2]===""){n.statusCode=416,n.setHeader("Content-Range",`bytes */${u}`),n.end();return}if(f[1]===""){const S=Number(f[2]);l=S>=u?0:u-S}else l=Number(f[1]),m=f[2]===""?u-1:Math.min(Number(f[2]),u-1);if(l>m||l>=u){n.statusCode=416,n.setHeader("Content-Range",`bytes */${u}`),n.end();return}n.statusCode=206,n.setHeader("Content-Range",`bytes ${l}-${m}/${u}`)}else n.statusCode=200;if(n.setHeader("Content-Length",String(m-l+1)),i.method==="HEAD"){n.end();return}const x=U(d.filePath,{start:l,end:m});x.on("error",()=>{n.headersSent||(n.statusCode=500),n.end()}),x.pipe(n)});await new Promise((i,n)=>{e.once("error",n),e.listen(0,t,()=>{e.removeListener("error",n),i()})});const o=e.address();g=e,w=t,v=typeof o=="object"&&o?o.port:0}async function E(){const t=g;g=null,w="",v=0,t&&await new Promise(e=>t.close(()=>e()))}async function T(t){g&&w===t||(g&&w!==t&&await E(),h||(h=ne(t).catch(e=>{throw h=null,e})),await h,h=null)}async function ie(t){const e=await P(t.filePath);if(!e.isFile())throw new Error(`path is not a file: ${t.filePath}`);const o=Date.now();Z(o),await T(t.host);const i=I(),n=y(t.filePath),a=t.ttlMs&&t.ttlMs>0?t.ttlMs:j,r=o+a;return p.set(i,{filePath:t.filePath,fileName:n,size:e.size,expiresAt:r}),{url:`http://${w}:${v}/d/${i}`,file_name:n,size:e.size,expires_at:r}}async function ue(){p.clear(),await E()}async function fe(t){return await T(t),v}async function le(t){const e=String(t.file_path??"").trim();if(!e)throw new Error("missing file_path");if(!A(e))throw new Error("file_path must be an absolute path");const o=await Y();if(!o)throw new Error("tailnet_unavailable: no tailnet IPv4 detected; ensure Tailscale is up on this host");const i=typeof t.ttl_ms=="number"?t.ttl_ms:void 0,n=await ie({filePath:e,host:o,ttlMs:i}),a=V(n.file_name)!==void 0;return{ok:!0,markdown:a?`![${n.file_name}](${n.url})`:`[${n.file_name}](${n.url})`,is_image:a,...n}}export{Y as detectTailnetIPv4,fe as ensureServerAndGetPort,ie as registerFileForServe,le as serveLocalFile,ue as stopFileServer};
1
+ import{randomUUID as A}from"node:crypto";import{execFile as U}from"node:child_process";import N from"node:http";import z from"node:https";import{createReadStream as O,createWriteStream as j}from"node:fs";import{stat as f,rename as B,unlink as T,access as V,readdir as G}from"node:fs/promises";import{basename as p,extname as P,isAbsolute as C,join as x,normalize as $,relative as J,resolve as M,sep as W}from"node:path";import*as X from"node:os";import{getCaCertPem as Y,getCaMobileConfig as Z,getLeafCredentials as K}from"./cert-store.js";const Q=600*1e3,L={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".bmp":"image/bmp",".tiff":"image/tiff",".tif":"image/tiff",".ico":"image/x-icon",".avif":"image/avif"};function q(e){return L[P(e).toLowerCase()]}const tt={".mp4":"video/mp4",".m4v":"video/mp4",".mov":"video/quicktime",".webm":"video/webm",".ogv":"video/ogg",".mkv":"video/x-matroska",".avi":"video/x-msvideo",".3gp":"video/3gpp",".ts":"video/mp2t"},et={".mp3":"audio/mpeg",".m4a":"audio/mp4",".aac":"audio/aac",".wav":"audio/wav",".ogg":"audio/ogg",".oga":"audio/ogg",".opus":"audio/opus",".flac":"audio/flac",".weba":"audio/webm"};function nt(e){const t=P(e).toLowerCase();return L[t]??tt[t]??et[t]}function _(e){const t=e.split(".");if(t.length!==4)return!1;const i=Number(t[0]),a=Number(t[1]);return!Number.isInteger(i)||!Number.isInteger(a)?!1:i===100&&a>=64&&a<=127}function it(){return new Promise(e=>{U("tailscale",["ip","-4"],{timeout:3e3},(t,i)=>{if(t){e(void 0);return}const a=i.trim().split(`
2
+ `)[0].trim();e(a&&_(a)?a:void 0)})})}function at(){const e=X.networkInterfaces();for(const t of Object.values(e))if(t){for(const i of t)if(!(i.family!=="IPv4"||i.internal)&&_(i.address))return i.address}}async function ot(){const e=await it();return e!==void 0?e:at()}const m=new Map;let h=null,u="",y=0,g=null,v=null,b=0,w=null;function rt(e){for(const[t,i]of m)i.expiresAt<=e&&m.delete(t)}const st=2*1024*1024*1024;function I(e){const t=e.socket.remoteAddress??"",i=t.startsWith("::ffff:")?t.slice(7):t;return _(i)}async function ct(e,t){const i=P(t),a=p(t,i);let n=x(e,t),o=0;for(;;)try{await V(n),o++,n=x(e,`${a}(${o})${i}`)}catch{return n}}function dt(e,t,i){return new Promise((a,n)=>{const o=j(t);let r=0,s=!1;const d=()=>{s||(s=!0,o.destroy(),T(t).catch(()=>{}))};e.on("data",c=>{r+=c.length,r>st&&(d(),n(Object.assign(new Error("file too large"),{code:413})))}),o.on("error",c=>{d(),n(c)}),e.on("error",c=>{d(),n(c)}),o.on("finish",async()=>{if(!s){s=!0;try{await B(t,i),a()}catch(c){T(t).catch(()=>{}),n(c)}}}),e.pipe(o)})}async function lt(e,t){if(!I(e)){t.statusCode=403,t.end("forbidden");return}const a=new URL(e.url??"/",`http://${e.headers.host}`).searchParams.get("dir")??"";if(!C(a)||$(a)!==M(a)){t.statusCode=400,t.end("invalid dir");return}const n=e.headers["x-filename"]??"",o=Array.isArray(n)?n[0]:n;let r;try{r=decodeURIComponent(o)}catch{r=o}if(!r||r.includes("/")||r.includes("\\")||r==="."||r===".."){t.statusCode=400,t.end("invalid filename");return}try{if(!(await f(a)).isDirectory()){t.statusCode=400,t.end("dir not found");return}}catch{t.statusCode=400,t.end("dir not found");return}const s=await ct(a,r),d=`${s}.${A()}.tmp`;try{await dt(e,d,s),t.statusCode=200,t.setHeader("Content-Type","application/json"),t.end(JSON.stringify({ok:!0,path:s,name:p(s)}))}catch(c){c.code===413?(t.statusCode=413,t.end("file too large")):(t.statusCode=500,t.end("upload failed"))}}function E(e,t,i,a,n,o){const r=nt(a);if(t.setHeader("Content-Type",r??"application/octet-stream"),t.setHeader("Content-Disposition",`${o&&r?"inline":"attachment"}; filename*=UTF-8''${encodeURIComponent(a)}`),t.setHeader("Accept-Ranges","bytes"),n===0){if(e.headers.range){t.statusCode=416,t.setHeader("Content-Range","bytes */0"),t.end();return}t.statusCode=200,t.setHeader("Content-Length","0"),t.end();return}let s=0,d=n-1;const c=e.headers.range;if(c){const l=/^bytes=(\d*)-(\d*)$/.exec(c.trim());if(!l||l[1]===""&&l[2]===""){t.statusCode=416,t.setHeader("Content-Range",`bytes */${n}`),t.end();return}if(l[1]===""){const k=Number(l[2]);s=k>=n?0:n-k}else s=Number(l[1]),d=l[2]===""?n-1:Math.min(Number(l[2]),n-1);if(s>d||s>=n){t.statusCode=416,t.setHeader("Content-Range",`bytes */${n}`),t.end();return}t.statusCode=206,t.setHeader("Content-Range",`bytes ${s}-${d}/${n}`)}else t.statusCode=200;if(t.setHeader("Content-Length",String(d-s+1)),e.method==="HEAD"){t.end();return}const S=O(i,{start:s,end:d});S.on("error",()=>{t.headersSent||(t.statusCode=500),t.end()}),S.pipe(t)}function ut(e,t){const i=String(e.url??"").split("?")[0],n=/^\/d\/([A-Za-z0-9-]+)$/.exec(i)?.[1],o=n?m.get(n):void 0;if(!o||o.expiresAt<=Date.now()){t.statusCode=404,t.end("not found");return}E(e,t,o.filePath,o.fileName,o.size,!0)}async function ft(e,t){if(!I(e)){t.statusCode=403,t.end("forbidden");return}const a=new URL(e.url??"/",`http://${e.headers.host}`).searchParams.get("path")??"";if(!C(a)||$(a)!==M(a)){t.statusCode=400,t.end("invalid path");return}let n;try{n=await f(a)}catch{t.statusCode=404,t.end("not found");return}if(!n.isFile()){t.statusCode=400,t.end("not a file");return}E(e,t,a,p(a),n.size,!1)}const H=5e4;async function pt(e){const t=[],i=[e];let a=0;for(;i.length>0&&!(t.length>=H);){const n=i.pop();let o;try{o=await G(n,{withFileTypes:!0})}catch{a++;continue}for(const r of o){if(t.length>=H)break;const s=x(n,r.name),d=J(e,s).split(W).join("/");if(!r.isSymbolicLink()){if(r.isDirectory())t.push({rel:d,is_dir:!0}),i.push(s);else if(r.isFile()){let c=0;try{c=(await f(s)).size}catch{a++;continue}t.push({rel:d,is_dir:!1,size:c,abs:s})}}}}return{entries:t,unreadable:a}}async function mt(e,t){if(!I(e)){t.statusCode=403,t.end("forbidden");return}const a=new URL(e.url??"/",`http://${e.headers.host}`).searchParams.get("path")??"";if(!C(a)||$(a)!==M(a)){t.statusCode=400,t.end("invalid path");return}let n;try{n=await f(a)}catch{t.statusCode=404,t.end("not found");return}if(!n.isDirectory()){t.statusCode=400,t.end("not a directory");return}const{entries:o,unreadable:r}=await pt(a),s=o.length>=H;t.statusCode=200,t.setHeader("Content-Type","application/json"),t.end(JSON.stringify({ok:!0,root_name:p(a),truncated:s,unreadable:r,entries:o}))}function ht(e){return/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&/Mobile/i.test(e)}async function gt(e,t){const i=String(e.headers["user-agent"]??""),n=new URL(e.url??"/",`http://${e.headers.host}`).searchParams.get("fmt");if(n==="mobileconfig"||n!=="crt"&&ht(i)){const s=await Z();t.statusCode=200,t.setHeader("Content-Type","application/x-apple-aspen-config"),t.setHeader("Content-Disposition",'attachment; filename="grix-tailnet-ca.mobileconfig"'),t.end(s);return}const r=await Y();t.statusCode=200,t.setHeader("Content-Type","application/x-x509-ca-cert"),t.setHeader("Content-Disposition",'attachment; filename="grix-tailnet-ca.crt"'),t.end(r)}function D(e,t){t.setHeader("Access-Control-Allow-Origin","*");const i=String(e.url??"").split("?")[0];if(i==="/ping"){t.statusCode=200,t.end("ok");return}if(i==="/ca"){gt(e,t).catch(()=>{t.headersSent||(t.statusCode=500,t.end("ca unavailable"))});return}if(i==="/upload"&&e.method==="POST"){lt(e,t).catch(()=>{t.headersSent||(t.statusCode=500,t.end("internal error"))});return}if(i==="/download"){ft(e,t).catch(()=>{t.headersSent||(t.statusCode=500,t.end("internal error"))});return}if(i==="/manifest"){mt(e,t).catch(()=>{t.headersSent||(t.statusCode=500,t.end("internal error"))});return}ut(e,t)}async function wt(e){const t=N.createServer(D);await new Promise((a,n)=>{t.once("error",n),t.listen(0,e,()=>{t.removeListener("error",n),a()})});const i=t.address();h=t,u=e,y=typeof i=="object"&&i?i.port:0}async function Ct(e){const{key:t,cert:i}=await K(e),a=z.createServer({key:t,cert:i},D);await new Promise((o,r)=>{a.once("error",r),a.listen(0,e,()=>{a.removeListener("error",r),o()})});const n=a.address();v=a,b=typeof n=="object"&&n?n.port:0}async function F(){const e=h,t=v;h=null,v=null,u="",y=0,b=0,await Promise.all([e&&new Promise(i=>e.close(()=>i())),t&&new Promise(i=>t.close(()=>i()))].filter(Boolean))}async function yt(e){v&&u===e||(w||(w=Ct(e).catch(t=>{throw w=null,t})),await w,w=null)}async function R(e){h&&u===e||(h&&u!==e&&await F(),g||(g=wt(e).catch(t=>{throw g=null,t})),await g,g=null),await yt(e)}async function vt(e){const t=await f(e.filePath);if(!t.isFile())throw new Error(`path is not a file: ${e.filePath}`);const i=Date.now();rt(i),await R(e.host);const a=A(),n=p(e.filePath),o=e.ttlMs&&e.ttlMs>0?e.ttlMs:Q,r=i+o;return m.set(a,{filePath:e.filePath,fileName:n,size:t.size,expiresAt:r}),{url:`https://${u}:${b}/d/${a}`,ca_install_url:`http://${u}:${y}/ca`,file_name:n,size:t.size,expires_at:r}}async function Ht(){m.clear(),await F()}async function kt(e){return await R(e),y}function At(){return b}async function Tt(e){const t=String(e.file_path??"").trim();if(!t)throw new Error("missing file_path");if(!C(t))throw new Error("file_path must be an absolute path");const i=await ot();if(!i)throw new Error("tailnet_unavailable: no tailnet IPv4 detected; ensure Tailscale is up on this host");const a=typeof e.ttl_ms=="number"?e.ttl_ms:void 0,n=await vt({filePath:t,host:i,ttlMs:a}),o=q(n.file_name)!==void 0;return{ok:!0,markdown:o?`![${n.file_name}](${n.url})`:`[${n.file_name}](${n.url})`,is_image:o,...n}}export{ot as detectTailnetIPv4,kt as ensureServerAndGetPort,At as getFileServerHttpsPort,vt as registerFileForServe,Tt as serveLocalFile,Ht as stopFileServer,pt as walkManifest};
@@ -1 +1 @@
1
- import{realpath as c,stat as f}from"node:fs/promises";import{resolve as d,dirname as u}from"node:path";import{listFiles as _,listWindowsDrives as s}from"./list-files.js";import{realHomeDir as n,normalizePlatformPath as h}from"./utils.js";async function k(t,a){if(process.platform==="win32"&&!t.parent_id)return{status:"ok",result:{files:await s(),current_path:""}};if(t.parent_id==="::root"){if(process.platform==="win32")return{status:"ok",result:{files:await s(),current_path:""}};t={...t,parent_id:"/"}}else t.parent_id==="::home"&&(t={...t,parent_id:n()});const l=a.fallbackDir??n(),o=t.parent_id?h(t.parent_id):null;if(process.platform!=="win32"&&o&&p(o))return{status:"failed",error_code:"path_not_found",error_msg:`Directory not found: ${t.parent_id}`};const i=o?d(o):l;let e;try{e=await c(i)}catch{return{status:"failed",error_code:"path_not_found",error_msg:`Directory not found: ${i}`}}try{(await f(e)).isDirectory()||(e=u(e))}catch{return{status:"failed",error_code:"path_not_accessible",error_msg:`Cannot access path: ${i}`}}try{return{status:"ok",result:{files:await _(e,{showHidden:t.show_hidden,allowedExtensions:t.allowed_extensions}),current_path:e}}}catch(r){return{status:"failed",error_code:"list_failed",error_msg:r instanceof Error?r.message:String(r)}}}function p(t){return/^[A-Za-z]:[/\\]/.test(t)||t.startsWith("\\\\")}export{k as handleFileListAction};
1
+ import{realpath as a,stat as u}from"node:fs/promises";import{resolve as _,dirname as h}from"node:path";import{listFiles as c,listWindowsDrives as d}from"./list-files.js";import{realHomeDir as f,normalizePlatformPath as w}from"./utils.js";async function b(t,s){if(process.platform==="win32"&&!t.parent_id)return{status:"ok",result:{files:await d(),current_path:""}};if(t.parent_id==="::root"){if(process.platform==="win32")return{status:"ok",result:{files:await d(),current_path:""}};t={...t,parent_id:"/"}}else t.parent_id==="::home"&&(t={...t,parent_id:f()});const e=s.fallbackDir??f(),r=t.parent_id?w(t.parent_id):null;if(process.platform!=="win32"&&r&&p(r))return l(e,t,"path_not_found",`Directory not found: ${t.parent_id}`);const o=r?_(r):e;let i;try{i=await a(o)}catch{return l(e,t,"path_not_found",`Directory not found: ${o}`)}try{(await u(i)).isDirectory()||(i=h(i))}catch{return l(e,t,"path_not_accessible",`Cannot access path: ${o}`)}try{return{status:"ok",result:{files:await c(i,{showHidden:t.show_hidden,allowedExtensions:t.allowed_extensions}),current_path:i}}}catch(n){return{status:"failed",error_code:"list_failed",error_msg:n instanceof Error?n.message:String(n)}}}async function l(t,s,e,r){try{const o=await a(t);return{status:"failed",result:{files:await c(o,{showHidden:s.show_hidden,allowedExtensions:s.allowed_extensions}),current_path:o},error_code:e,error_msg:r}}catch{return{status:"failed",error_code:e,error_msg:r}}}function p(t){return/^[A-Za-z]:[/\\]/.test(t)||t.startsWith("\\\\")}export{b as handleFileListAction};
@@ -1 +1 @@
1
- const a=[{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"],description:"Query action type."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},keyword:{type:"string",description:"Search keyword."},limit:{type:"integer",description:"Max results."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. Each link is one-time and expires, so call this again to produce a fresh link every time you deliver a file. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds (default 10 minutes)."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Agent introduction (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the text introduction of one of your owner's agents, identified by its numeric agent ID.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id","introduction"]},validation:{required:["agent_id","introduction"],properties:{agent_id:{type:"string"},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:"Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend opens (or reuses) a private session between the owner and that agent, binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session as the owner so the agent starts working.",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."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Use ONLY to relay on the owner's behalf into one of the owner's OTHER sessions that you are not part of (e.g. you were dispatched to work and need to drop a note to the owner elsewhere). NEVER use this to send your own reply in a session you are conversing in \u2014 that would make your words show up as the owner's message. To answer in your current conversation, reply normally (or use grix_message_send to send as yourself). The owner must be a member of the target session, and you (the agent) must NOT be a member of it \u2014 sending into a session you belong to is rejected.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}}],d=[{name:"grix_reply",description:"Send a reply message to the specified session. Supports streaming in chunks; the frontend will automatically aggregate them into one complete message. Any content meant for the user \u2014 including your final conclusion at the end of a turn \u2014 MUST be sent through this tool; text written outside this tool is not delivered to the user.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),b=new Set(a.map(e=>e.name)),v=new Set(d.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,w=/[A-Za-z0-9._-]+/;function z(e){return!!(b.has(e)||v.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const k=[...a,...d],x=[...k,...p],P=new Map(x.map(e=>[e.name,e]));function R(e,t){return e==="reply"?{name:"grix_reply",args:_("grix_reply",{event_id:t.event_id,session_id:t.chat_id,text:t.text,quoted_message_id:t.reply_to,is_final:t.final})}:e==="complete"?{name:"grix_complete",args:_("grix_complete",{event_id:t.event_id,status:t.status,msg:t.msg,code:t.code})}:{name:e,args:t}}function l(e){const t=String(e??"").trim();return t?t.match(I)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return m(n);const i=t.match(/(?:chat_id|session_id)\s*=\s*"([A-Za-z0-9._-]+)"/)?.[1];if(i)return i;const o=t.match(/[A-Za-z0-9._-]+/g)??[];for(const s of o)if(!(s==="event_id"||s==="chat_id"||s==="session_id")&&s.length>0)return s;return t.match(w)?.[0]}function m(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function _(e,t){if(e!=="grix_reply"&&e!=="grix_complete")return t;const n={...t},i=l(n.event_id);if(i&&(n.event_id=i),e==="grix_reply"){const o=m(i),r=String(n.session_id??""),s=u(n.session_id),f=/\bevent_id\b|["'<>\s]/.test(r);s&&!(f&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function U(e){return c.has(e)}function G(e,t){switch(e){case"grix_query":return S(t);case"grix_group":return A(t);case"grix_message_send":return q(t);case"grix_message_unsend":return T(t);case"grix_file_link":return M(t);case"grix_admin":return C(t);case"grix_call_owner":return O(t);case"grix_agent_update":return D(t);case"grix_dispatch_agent":return E(t);case"grix_session_send":return L(t);case"grix_chat_state_query":return N(t);case"grix_chat_state_update":return j(t);default:throw new Error(`Unknown tool: ${e}`)}}const g={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search"};function S(e){const t=String(e.action??""),n=g[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.keyword!=null&&(i.keyword=e.keyword),e.limit!=null&&(i.limit=e.limit),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}const y={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function A(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function q(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 T(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function M(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}const h={create_agent:"agent_api_create",list_categories:"agent_category_list",create_category:"agent_category_create",update_category:"agent_category_update",assign_category:"agent_category_assign",rotate_api_key:"agent_api_key_rotate"};function O(e){return{action:"call_owner",params:{session_id:e.session_id}}}function D(e){return{action:"agent_introduction_update",params:{agent_id:e.agent_id,introduction:e.introduction}}}function E(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task}}}function L(e){return{action:"session_send",params:{session_id:e.session_id,content:e.content}}}function N(e){const t={};return e.session_id!=null&&(t.session_id=e.session_id),e.page!=null&&(t.page=e.page),e.page_size!=null&&(t.page_size=e.page_size),e.state!=null&&(t.state=e.state),{action:"chat_state_query",params:t}}function j(e){const t={session_id:e.session_id,state:e.state};return e.reason!=null&&(t.reason=e.reason),{action:"chat_state_update",params:t}}function C(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_admin action: ${t}`);const i={};return e.agentName!=null&&(i.agent_name=e.agentName),e.introduction!=null&&(i.introduction=e.introduction),e.isMain!=null&&(i.is_main=e.isMain),e.agentId!=null&&(i.agent_id=e.agentId),e.categoryId!=null&&(i.category_id=e.categoryId),e.name!=null&&(i.name=e.name),e.parentId!=null&&(i.parent_id=e.parentId),e.sortOrder!=null&&(i.sort_order=e.sortOrder),{action:n,params:i}}const W=new Set([...Object.values(g),...Object.values(y),...Object.values(h),"send_msg","delete_msg","file_link","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update"]),Q={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Q as ACCESS_CONTROL_ACTION_MAP,k as ALL_TOOLS,d as EVENT_TOOLS,x as EXPOSED_TOOLS,W as PHASE1_INVOKE_ACTIONS,b as PHASE1_TOOL_NAMES,v as PHASE2_TOOL_NAMES,a as TOOLS,p as TOOL_ALIASES,P as TOOL_MAP,U as isAlias,z as isGrixInternalToolName,R as mapToolAlias,_ as normalizeEventToolArgs,G as toolCallToInvoke};
1
+ const a=[{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"],description:"Query action type."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},keyword:{type:"string",description:"Search keyword."},limit:{type:"integer",description:"Max results."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. Each link is one-time and expires, so call this again to produce a fresh link every time you deliver a file. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds (default 10 minutes)."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Agent introduction (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the text introduction of one of your owner's agents, identified by its numeric agent ID.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id","introduction"]},validation:{required:["agent_id","introduction"],properties:{agent_id:{type:"string"},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:"Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend opens (or reuses) a private session between the owner and that agent, binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session as the owner so the agent starts working.",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."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Use ONLY to relay on the owner's behalf into one of the owner's OTHER sessions that you are not part of (e.g. you were dispatched to work and need to drop a note to the owner elsewhere). NEVER use this to send your own reply in a session you are conversing in \u2014 that would make your words show up as the owner's message. To answer in your current conversation, reply normally (or use grix_message_send to send as yourself). The owner must be a member of the target session, and you (the agent) must NOT be a member of it \u2014 sending into a session you belong to is rejected.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}}],d=[{name:"grix_reply",description:"Send a reply message to the specified session. Supports streaming in chunks; the frontend will automatically aggregate them into one complete message. Any content meant for the user \u2014 including your final conclusion at the end of a turn \u2014 MUST be sent through this tool; text written outside this tool is not delivered to the user.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),b=new Set(a.map(e=>e.name)),v=new Set(d.map(e=>e.name)),w=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,I=/[A-Za-z0-9._-]+/;function z(e){return!!(b.has(e)||v.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const k=[...a,...d],x=[...k,...p],P=new Map(x.map(e=>[e.name,e]));function R(e,t){return e==="reply"?{name:"grix_reply",args:_("grix_reply",{event_id:t.event_id,session_id:t.chat_id,text:t.text,quoted_message_id:t.reply_to,is_final:t.final})}:e==="complete"?{name:"grix_complete",args:_("grix_complete",{event_id:t.event_id,status:t.status,msg:t.msg,code:t.code})}:{name:e,args:t}}function l(e){const t=String(e??"").trim();return t?t.match(w)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return m(n);const i=t.match(/(?:chat_id|session_id)\s*=\s*"([A-Za-z0-9._-]+)"/)?.[1];if(i)return i;const o=t.match(/[A-Za-z0-9._-]+/g)??[];for(const s of o)if(!(s==="event_id"||s==="chat_id"||s==="session_id")&&s.length>0)return s;return t.match(I)?.[0]}function m(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function _(e,t){if(e!=="grix_reply"&&e!=="grix_complete")return t;const n={...t},i=l(n.event_id);if(i&&(n.event_id=i),e==="grix_reply"){const o=m(i),r=String(n.session_id??""),s=u(n.session_id),f=/\bevent_id\b|["'<>\s]/.test(r);s&&!(f&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function U(e){return c.has(e)}function G(e,t){switch(e){case"grix_query":return S(t);case"grix_group":return A(t);case"grix_message_send":return T(t);case"grix_message_unsend":return q(t);case"grix_file_link":return M(t);case"grix_admin":return C(t);case"grix_call_owner":return O(t);case"grix_agent_update":return D(t);case"grix_dispatch_agent":return E(t);case"grix_session_send":return L(t);case"grix_chat_state_query":return N(t);case"grix_chat_state_update":return j(t);default:throw new Error(`Unknown tool: ${e}`)}}const g={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search"};function S(e){const t=String(e.action??""),n=g[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.keyword!=null&&(i.keyword=e.keyword),e.limit!=null&&(i.limit=e.limit),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}const y={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function A(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function T(e){const t={session_id:e.sessionId,msg_type:e.msgType??1,content:e.content};return e.quotedMessageId!=null&&(t.quoted_message_id=e.quotedMessageId),e.threadId!=null&&(t.thread_id=e.threadId),{action:"send_msg",params:t}}function q(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function M(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}const h={create_agent:"agent_api_create",list_categories:"agent_category_list",create_category:"agent_category_create",update_category:"agent_category_update",assign_category:"agent_category_assign",rotate_api_key:"agent_api_key_rotate"};function O(e){return{action:"call_owner",params:{session_id:e.session_id}}}function D(e){return{action:"agent_introduction_update",params:{agent_id:e.agent_id,introduction:e.introduction}}}function E(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task}}}function L(e){return{action:"session_send",params:{session_id:e.session_id,content:e.content}}}function N(e){const t={};return e.session_id!=null&&(t.session_id=e.session_id),e.page!=null&&(t.page=e.page),e.page_size!=null&&(t.page_size=e.page_size),e.state!=null&&(t.state=e.state),{action:"chat_state_query",params:t}}function j(e){const t={session_id:e.session_id,state:e.state};return e.reason!=null&&(t.reason=e.reason),{action:"chat_state_update",params:t}}function C(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_admin action: ${t}`);const i={};return e.agentName!=null&&(i.agent_name=e.agentName),e.introduction!=null&&(i.introduction=e.introduction),e.isMain!=null&&(i.is_main=e.isMain),e.agentId!=null&&(i.agent_id=e.agentId),e.categoryId!=null&&(i.category_id=e.categoryId),e.name!=null&&(i.name=e.name),e.parentId!=null&&(i.parent_id=e.parentId),e.sortOrder!=null&&(i.sort_order=e.sortOrder),{action:n,params:i}}const W=new Set([...Object.values(g),...Object.values(y),...Object.values(h),"send_msg","delete_msg","file_link","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update"]),Q={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Q as ACCESS_CONTROL_ACTION_MAP,k as ALL_TOOLS,d as EVENT_TOOLS,x as EXPOSED_TOOLS,W as PHASE1_INVOKE_ACTIONS,b as PHASE1_TOOL_NAMES,v as PHASE2_TOOL_NAMES,a as TOOLS,p as TOOL_ALIASES,P as TOOL_MAP,U as isAlias,z as isGrixInternalToolName,R as mapToolAlias,_ as normalizeEventToolArgs,G as toolCallToInvoke};
@@ -0,0 +1 @@
1
+ import e from"node:os";function t(){return e.hostname()}export{t as getMachineName};
@@ -1 +1 @@
1
- import{readJSONFile as o,writeJSONFileAtomic as t}from"./json-file.js";import{EventBuffer as i}from"./event-buffer.js";import{QuotedMessageStream as m}from"./quoted-message-stream.js";import{resolveClientVersion as l}from"./client-version.js";import{isUserVisibleAgentMessagePhase as x}from"./codex-output-policy.js";export{i as EventBuffer,m as QuotedMessageStream,x as isUserVisibleAgentMessagePhase,o as readJSONFile,l as resolveClientVersion,t as writeJSONFileAtomic};
1
+ import{readJSONFile as o,writeJSONFileAtomic as t}from"./json-file.js";import{EventBuffer as s}from"./event-buffer.js";import{QuotedMessageStream as f}from"./quoted-message-stream.js";import{resolveClientVersion as p}from"./client-version.js";import{isUserVisibleAgentMessagePhase as l}from"./codex-output-policy.js";import{getMachineName as g}from"./host-info.js";export{s as EventBuffer,f as QuotedMessageStream,g as getMachineName,l as isUserVisibleAgentMessagePhase,o as readJSONFile,p as resolveClientVersion,t as writeJSONFileAtomic};
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: grix-egg
3
+ description: Install a Grix egg skill package into your own skill directory. Use when you receive an instruction containing a line like "技能包: <URL>" and are asked to install / incubate it as a skill for yourself.
4
+ trigger: 当收到含「技能包: <URL>」的安装指令、要求把虾蛋技能包装进自己的技能目录时
5
+ ---
6
+
7
+ # Grix Egg — 技能包自装
8
+
9
+ 虾蛋(egg)孵化对你这类 agent 来说就是一件事:**把一个技能包装进你自己的技能目录**,让你之后能用上它。
10
+
11
+ 当你收到一条带有 `技能包: <URL>` 的安装指令时,就用本技能完成安装、自检,再回发一张安装状态卡片。整件事由你自己在本机完成——没有别的 agent 会替你装,主 agent 也碰不到你的技能目录。
12
+
13
+ ## 指令里会给你什么
14
+
15
+ - `技能包: <URL>` —— 要安装的技能包下载地址(必有)。
16
+ - `install_id: <ID>` —— 这次安装的编号,回发状态卡片时要原样带上(必有)。
17
+ - `grix agent id: <ID>` —— 你自己的 grix agent id,回发状态卡片时作为 `target_agent_id`(可能有)。
18
+
19
+ ## 你要做的事
20
+
21
+ 1. 取出 `技能包:` 后面的下载 URL。
22
+ 2. 确定你自己的技能目录:就是**本 grix-egg 技能所在目录的上一级**。例如本技能在 `~/.codex/skills/grix-egg/`,技能目录就是 `~/.codex/skills/`;在 `~/.claude/skills/grix-egg/` 则是 `~/.claude/skills/`,以此类推。
23
+ 3. 下载技能包、解压。
24
+ 4. 把解压出来的每个技能(含 `SKILL.md` 的目录)整目录拷进你的技能目录。
25
+ 5. 确认技能目录里已经出现新技能的 `SKILL.md`。
26
+ 6. **回发安装状态卡片**(见下),让平台把这次安装收口成成功/失败。
27
+ 7. 再用一句自然语言告诉用户:装好了哪个/哪些技能。
28
+
29
+ ## 第 2~5 步:安装(用 shell 执行)
30
+
31
+ 把两处尖括号替换成真实值后执行。`SKILLS_DIR` 取本技能所在目录的上一级。
32
+
33
+ ```bash
34
+ set -e
35
+ URL="<技能包 URL>"
36
+ SKILLS_DIR="<你的技能目录绝对路径>"
37
+
38
+ TMP="$(mktemp -d)"
39
+ curl -fsSL "$URL" -o "$TMP/skill.zip"
40
+ mkdir -p "$TMP/unpacked"
41
+ unzip -o -q "$TMP/skill.zip" -d "$TMP/unpacked"
42
+
43
+ # 把每个含 SKILL.md 的技能目录整目录拷进技能目录
44
+ found=0
45
+ while IFS= read -r -d '' f; do
46
+ d="$(dirname "$f")"
47
+ cp -R "$d" "$SKILLS_DIR/"
48
+ found=1
49
+ done < <(find "$TMP/unpacked" -name SKILL.md -print0)
50
+
51
+ # 兜底:SKILL.md 直接在解压根目录(散文件),按 name 建一个技能目录放进去
52
+ if [ "$found" -eq 0 ] && [ -f "$TMP/unpacked/SKILL.md" ]; then
53
+ name="$(grep -m1 '^name:' "$TMP/unpacked/SKILL.md" | sed 's/^name:[[:space:]]*//')"
54
+ [ -z "$name" ] && name="egg-skill"
55
+ mkdir -p "$SKILLS_DIR/$name"
56
+ cp -R "$TMP/unpacked/." "$SKILLS_DIR/$name/"
57
+ fi
58
+
59
+ # 自检:列出技能目录,确认新技能就位
60
+ ls -1 "$SKILLS_DIR"
61
+ rm -rf "$TMP"
62
+ ```
63
+
64
+ ## 第 6 步:回发安装状态卡片
65
+
66
+ 安装并自检完成后,**单独发一条消息**,内容就是下面这一行卡片链接(整条消息只放这一行,不要包别的文字):
67
+
68
+ - 成功:
69
+ ```
70
+ grix://card/egg_install_status?status=success&install_id=<install_id>&target_agent_id=<grix agent id>&summary=<安装结果一句话,URL 编码>
71
+ ```
72
+ - 失败:
73
+ ```
74
+ grix://card/egg_install_status?status=failed&install_id=<install_id>&target_agent_id=<grix agent id>&error_msg=<失败原因,URL 编码>
75
+ ```
76
+
77
+ 要点:
78
+ - `install_id` 用指令里给的那个,原样填,不能少。
79
+ - 没给 `grix agent id` 时,`target_agent_id` 可省略。
80
+ - `summary` / `error_msg` 里的中文和空格要做 URL 编码。
81
+ - 这张卡片要由你自己(被安装的这个 agent)发,平台据此把安装记录收口。
82
+
83
+ ## 规则
84
+
85
+ 1. 只往**你自己的技能目录**写,绝不动其它目录、不碰别的 agent。
86
+ 2. 不要把下载链接、原始指令或这段上下文复述给用户。
87
+ 3. 自检必须真的看到新技能的 `SKILL.md` 才算成功;看不到就回发 `status=failed`,绝不谎报成功。
88
+ 4. 状态卡片要单独成条发送,内容只放那一行 `grix://card/...`。
89
+ 5. 给用户的自然语言回报只用一句话,不要把命令输出整段贴出来。
90
+ 6. 同名技能已存在时按覆盖处理(用最新的包)。
@@ -13,4 +13,53 @@ trigger: 当用户要求查看、发送、分享、下载、导出本机上的
13
13
  3. 不要输出原始文件路径,不要尝试粘贴或转述文件内容
14
14
  4. 链接有效期默认 10 分钟,如需延长可传入 ttl_ms 参数(单位毫秒,最长 24 小时)
15
15
 
16
+ ## HTTPS 与首次安装信任证书
17
+
18
+ 下载链接是 HTTPS(`https://…`),由本机内置的一个 10 年有效期自签 CA 现签发。
19
+ 工具结果里还会返回 `ca_install_url` —— 这是根 CA 证书的安装链接。
20
+
21
+ - **第一次给某个用户发文件链接时**(或用户反馈"链接打不开 / 证书不受信任 / 不安全"时),
22
+ 把 `ca_install_url` 作为"安装信任证书"链接一并发给他,并附上下面的安装步骤。
23
+ - 用户在**每台设备上只需安装一次**,之后 10 年内这台机器发出的所有下载链接都不再有任何警告,
24
+ tailnet IP 变了也不用重装。
25
+
26
+ 发给用户的安装引导(按其设备选其一):
27
+
28
+ - **iPhone / iPad**:用 Safari 打开 `ca_install_url`,会直接弹出"此网站正尝试下载一个描述文件" → 允许 →
29
+ 到「设置」顶部出现"已下载描述文件",点进去安装 →
30
+ 再到「设置 → 通用 → 关于本机 → 证书信任设置」,把该证书开关打开(开启完全信任)。这一步必须做,否则系统仍不信任。
31
+ (iOS 必须用 Safari 打开,其他浏览器不会触发描述文件安装。服务端已按设备自动返回 .mobileconfig,无需手动加参数。)
32
+ - **Mac**:点链接下载 `.crt` → 双击用「钥匙串访问」打开 → 找到该证书 → 双击 →「信任」展开 →「使用此证书时」选"始终信任"。
33
+ - **Android**:点 `ca_install_url` 下载 `.crt` →「设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书」选择刚下载的文件安装
34
+ (新版安卓不支持浏览器内一键安装,必须走系统设置这一步)。
35
+ - **Windows**:双击 `.crt` →「安装证书」→ 选存储位置 →「将所有证书放入下列存储」→ 选"受信任的根证书颁发机构"。
36
+
16
37
  如果 grix_file_link 调用失败(如未连接 Tailscale),告诉用户文件的本地路径,让他们自行获取。
38
+
39
+ ## 适用范围(重要):这套证书只管 grix_file_link 自己的服务
40
+
41
+ 上面这张内置 CA 和它签发的证书,**只让 grix_file_link 起的下载服务(本机 tailnet 地址)被设备信任**。它不负责、也解决不了用户自己另起的其它服务。
42
+
43
+ 当用户在 tailnet 上**自行启动了别的 HTTPS 服务**(自建网站 / API / 媒体服务等),用浏览器打开报证书错误时,要分清这是另一回事:
44
+
45
+ - 设备只信任「它亲自装过的那张 CA 签出来的证书」。自建服务用的是它自己的 CA(如 mkcert),设备没装过,所以报 `不受信任 / 冒充 / NET::ERR_CERT_AUTHORITY_INVALID`——这跟有没有装 Grix 的 CA 无关。
46
+ - 想让自建服务也被信任,只有两条路:
47
+ 1. 让该服务改用一张设备已信任的 CA 来签证书,并且叶子证书有效期 **≤ 398 天**(否则即使信任了 CA,仍会报 `NET::ERR_CERT_VALIDITY_TOO_LONG`);
48
+ 2. 或者干脆别让用户开自己的 HTTPS,把要分享的文件改走 grix_file_link,由本机内置服务发出来,自动被信任、有效期也合规。
49
+ - 注意:Grix 内置 CA 的**私钥只留在跑连接器的这台机器上、不对外发**(`ca_install_url` 只给公钥证书,不给私钥)。因此只有**与连接器同一台机器**上的服务能借这张 CA 来签证书;其它机器上的服务必须自带 CA,并在每台设备各装一次。
50
+
51
+ 简而言之:能用 grix_file_link 发的就用它发,最省事;自建服务的证书信任问题不在本技能职责内,需在那个服务侧自行解决。
52
+
53
+ ## 两条信任路:Grix app 内 vs 系统浏览器(排障先分清)
54
+
55
+ 同一个链接,在 **Grix app 里打开** 和在 **系统浏览器(Safari / Chrome)里打开**,走的是两套完全独立的证书信任机制,报错和解法都不同。用户反馈"打不开 / 证书报错"时,先问清他是在哪条路上看的。
56
+
57
+ - **Grix app 内**(图片预览、应用内下载等走 app 自带的网络栈):app 内置了一条信任规则——只要链接是 tailnet 地址(`100.64.0.0/10` 段)、且证书签发者名字里含 `Grix Tailnet Local CA`,就直接放行,**用户不需要在设备上安装任何证书**。所以 grix_file_link 的链接在 app 里通常开箱即用、零安装。app 内若仍打不开,多半是没走 tailnet IP、或证书签发者名字不符,而不是"没装证书"。
58
+ - **系统浏览器(Safari / Chrome)**:走的是手机 / 电脑的系统信任库,跟 app 那条规则毫无关系。必须按上面《HTTPS 与首次安装信任证书》把根 CA 装好(iOS 装 .mobileconfig 并开完全信任,安卓走系统设置,等等)。用户截图里出现的 `NET::ERR_CERT_AUTHORITY_INVALID`、`冒充`、`NET::ERR_CERT_VALIDITY_TOO_LONG`,几乎都是系统浏览器这条路,引导他装 CA 即可。
59
+
60
+ 对**自建服务**同理,按用户要在哪看来定:
61
+
62
+ - 只需要在 **Grix app 内**展示:自建服务无需向用户分发私钥、也无需装任何证书——只要①服务挂在 tailnet IP 上、②把它那张 CA 的签发者名字带上 `Grix Tailnet Local CA`,app 就会信任。
63
+ - 需要在 **系统浏览器** 里打开:app 那条规则不起作用,仍需在每台设备装上该服务自己的 CA(见上一节《适用范围》)。
64
+
65
+ 注意:app 内这条是**按签发者名字字符串匹配**,不是按某把 CA 的公钥指纹绑定。它仅作为私有 tailnet 内的便利取舍,**不是强安全边界**——同一 tailnet 内任何人只要把 CA 命名成相同前缀就会被 app 信任。不要据此把它当作可对抗攻击者的信任根。
package/dist/log.js CHANGED
@@ -1,3 +1,3 @@
1
- import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as i}from"node:path";import{homedir as m}from"node:os";const n=i(m(),".grix"),s={base:n,config:i(n,"config"),log:i(n,"log"),data:i(n,"data")};function S(){for(const o of Object.values(s))f(o)||l(o,{recursive:!0})}let a=null;function $(){const o=new Date().toISOString().slice(0,10),r=i(s.log,`grix-acp-${o}.log`);a=g(r,{flags:"a"})}function c(){return new Date().toISOString().slice(11,19)}const u={info(o,r,...t){const e=`${c()} [${o}] ${r}${t.length?" "+t.map(String).join(" "):""}`;console.log(e),a?.write(e+`
2
- `)},error(o,r,...t){const e=`${c()} [${o}] ERROR ${r}${t.length?" "+t.map(String).join(" "):""}`;console.error(e),a?.write(e+`
1
+ import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as t}from"node:path";import{homedir as m}from"node:os";const i=t(m(),".grix"),s={base:i,config:t(i,"config"),log:t(i,"log"),data:t(i,"data")};function S(){for(const o of Object.values(s))f(o)||l(o,{recursive:!0})}let a=null;function $(){const o=new Date().toISOString().slice(0,10),r=t(s.log,`grix-acp-${o}.log`);a=g(r,{flags:"a"})}function c(){return new Date().toISOString().slice(11,19)}const u={info(o,r,...n){const e=`${c()} [${o}] ${r}${n.length?" "+n.map(String).join(" "):""}`;console.log(e),a?.write(e+`
2
+ `)},error(o,r,...n){const e=`${c()} [${o}] ERROR ${r}${n.length?" "+n.map(String).join(" "):""}`;console.error(e),a?.write(e+`
3
3
  `)}};export{s as GRIX_PATHS,S as ensureGrixDirs,$ as initLogger,u as log};