grix-connector 4.2.4 → 4.2.6

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,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bridgeVersion": "4.2.4",
4
- "tarball": "grix-dsh-bridge-4.2.4.tgz",
5
- "size": 21979,
6
- "unpackedSize": 87631,
7
- "shasum": "d17cee0fbef1fe0726f1df062efd56a49504561e",
8
- "integrity": "sha512-L6QrmOl0Rd84Ywrwn0NDcdO9Dn8dFGY2o93N/f7mNTxBPDdAvVQs+jYuGzJNPluEXq5X+WVjd0hy27LN3yPRCg=="
3
+ "bridgeVersion": "4.2.6",
4
+ "tarball": "grix-dsh-bridge-4.2.6.tgz",
5
+ "size": 22490,
6
+ "unpackedSize": 89865,
7
+ "shasum": "c7df7dd6d579c151bb30160a37dd1df22be88739",
8
+ "integrity": "sha512-lENzrDzvim4zMtv3DFbboB/c1wKG4qWWijt06ihJunQ5EXhPm9Yb6DanBdyrH4T22IE3VJzpYej+0LGi93pEaA=="
9
9
  }
@@ -1 +1 @@
1
- const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},keyword:{type:"string",description:"Search keyword."},sessionType:{type:"integer",enum:[1,2],description:"Filter by session type (session_search only): 1=private chat, 2=group chat. Omit to return all."},limit:{type:"integer",description:"Max results. message_history defaults to 1 when omitted."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"]},id:{type:"string"},keyword:{type:"string",maxLength:200},sessionType:{type:"integer",enum:[1,2]},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_audit_data",description:"Read connector-backed conversation audit replay data from Grix/AIBot: manifest, span pages, and content chunks.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"],description:"Audit read action. Start with get_manifest, then call list_spans and/or get_content_chunk based on the manifest."},auditId:{type:"string",description:"Audit ID returned by the audited turn metadata."},revision:{type:"integer",description:"Optional manifest revision to read. Usually use the revision returned by get_manifest."},cursor:{type:"string",description:"Opaque pagination cursor returned by list_spans or get_content_chunk."},limit:{type:"integer",description:"Max span items for list_spans."},contentId:{type:"string",description:"Content reference ID from manifest.content_refs (get_content_chunk only)."},maxBytes:{type:"integer",description:"Max UTF-8 bytes to return for get_content_chunk."}},required:["action","auditId"]},validation:{required:["action","auditId"],properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"]},auditId:{type:"string"},revision:{type:"integer",minimum:1},cursor:{type:"string",maxLength:4096},limit:{type:"integer",minimum:1,maximum:200},contentId:{type:"string"},maxBytes:{type:"integer",minimum:1,maximum:131072}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform. Do NOT use this to answer the inbound event you are currently handling \u2014 answer in plain text (or the reply tool where available) instead, otherwise the user receives duplicate messages.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. The link is reachable only inside the tailnet, so just send it as-is \u2014 no need to worry about or mention any link lifetime. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds. Leave unset to use the long default; set only if you deliberately want a short-lived link."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_file_upload",description:"Upload a local file to the Grix platform and send it as a media message in the target session. Supports images (jpg/png/webp/gif/bmp/heic/heif), videos (mp4/mov/m4v/webm/mkv/avi), documents (pdf/doc/docx/xls/xlsx/ppt/pptx/txt/md/csv/json/xml), and archives (zip/rar/7z/tar/gz). Max 50 MB. Use this instead of grix_file_link when the file should appear as a native attachment in the chat (visible inline for images/videos), rather than a tailnet download link.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file to upload."},session_id:{type:"string",description:"Target session ID to send the file to."},caption:{type:"string",description:"Optional text caption for the media message."},reply_to_message_id:{type:"string",description:"Optional message ID to quote/reply to."}},required:["file_path","session_id"]},validation:{required:["file_path","session_id"],properties:{file_path:{type:"string",maxLength:4096},session_id:{type:"string"},caption:{type:"string",maxLength:2e3},reply_to_message_id:{type:"string"}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Professional behavioral introduction describing the Agent purpose, responsibilities, operating expectations, and boundaries (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the display name and/or text introduction of one of your owner's agents, identified by its numeric agent ID. Provide agent_name, introduction, or both \u2014 at least one is required.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},agent_name:{type:"string",description:"New display name (max 100 characters, must be unique among the owner's agents)."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id"]},validation:{required:["agent_id"],properties:{agent_id:{type:"string"},agent_name:{type:"string",maxLength:100},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:`Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend creates a NEW private session between the owner and that agent for each dispatch (it does not reuse past sessions), binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session AS THE OWNER so the agent starts working. Because the task is delivered as the owner, write it in the owner's first-person voice and tone \u2014 phrase it the way the owner would speak directly to the agent (e.g. "\u5E2E\u6211\u2026", "\u4F60\u53BB\u2026"), NOT as a third-person relay or as yourself narrating on the owner's behalf. Provide a short title summarizing the core of the task \u2014 it becomes the new session's title; if omitted the backend derives one from the task text.`,inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},cwd:{type:"string",description:"Absolute working directory where the agent should do the work."},task:{type:"string",description:"Text description of the task to perform, written in the owner's first-person voice and tone \u2014 it is delivered into the session as the owner, so phrase it as the owner speaking directly to the agent, not as a third-person relay."},title:{type:"string",description:"Optional short title (a few words) summarizing the core of the task; used as the new session's title. If omitted, the backend derives a title from the task text."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4},title:{type:"string",maxLength:255}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Dispatch callbacks (report_dispatch_result) MUST use this tool, even when you are a member of the callback session: a message sent as yourself cannot quote-wake the dispatcher agent, and your membership is not checked. NEVER use this for your own ordinary replies in a conversation \u2014 that would make your words show up as the owner's message; reply normally (or use grix_message_send to send as yourself) for those. The owner must be a member of the target session. Requires the Send as Owner permission scope \u2014 if rejected for missing permission, surface the error as-is so the owner can grant it; do not fall back to grix_message_send. Optional quoted_message_id quotes a message in the same session (used by dispatch callbacks to wake the dispatcher agent).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."},quoted_message_id:{type:"string",description:"Optional message ID in the target session to quote/reply to.",pattern:"^[1-9]\\d*$"}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4},quoted_message_id:{type:"string",pattern:"^[1-9]\\d*$"}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). When session_id identifies one completed session, the entry also includes exactly one clean final_result; non-completed and list queries never include message content. Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_widget_visitor_ban",description:"Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Widget visitor session ID (required)."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_skill_set",description:"Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (max 100 characters); unique within the owner. Used as the identifier and the synced directory name."},content:{type:"string",description:"Full SKILL.md text. Pass an empty string to delete the skill by name."}},required:["name","content"]},validation:{required:["name","content"],properties:{name:{type:"string",maxLength:100},content:{type:"string",maxLength:262144}}}},{name:"grix_skill_get",description:"Read one of your owner's custom skills by name (returns its full SKILL.md content), or list the owner's skill library when no name is given. Use this before editing to fetch current content, then call grix_skill_set with the revised text.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name to fetch. Omit to list all skills (names + versions) instead."}}},validation:{required:[],properties:{name:{type:"string",maxLength:100}}}}],a=[{name:"grix_reply",description:"Send your final reply \u2014 the conclusion the user is waiting for \u2014 to the specified session. This is the message the user treats as your answer; deliver your final result here, and use plain text only for brief progress notes while you work. Supports streaming in chunks; the frontend automatically aggregates them into one complete message. The connector quotes the message being answered automatically, so quoted_message_id is optional (set it only to quote a different earlier message).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],v={grix_dispatch_agent:"grix-agent-dispatch",grix_agent_update:"grix-agent-dispatch",grix_query:"grix-query",grix_audit_data:"grix-audit-data",grix_group:"grix-group",grix_message_send:"message-send",grix_message_unsend:"message-unsend",grix_admin:"grix-admin",grix_call_owner:"grix-owner-relay",grix_session_send:"grix-owner-relay",grix_chat_state_query:"grix-chat-state",grix_chat_state_update:"grix-chat-state",grix_status:"grix-chat-state",grix_access_control:"grix-access-control",grix_widget_visitor_ban:"grix-widget-visitor-ban",grix_file_link:"tailnet-file-share"};function w(e){return` Before calling this tool, follow the \`${e}\` skill's procedure first; do not invoke this tool directly without going through that skill's guidance.`}for(const e of[...r,...a]){const t=v[e.name];t&&(e.description+=w(t))}const p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),x=new Set(r.map(e=>e.name)),k=new Set(a.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,S=/[A-Za-z0-9._-]+/;function H(e){return!!(x.has(e)||k.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const q=[...r,...a],A=[...q,...p],Z=new Map(A.map(e=>[e.name,e]));function K(e,t){return e==="reply"?{name:"grix_reply",args:m("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:m("grix_complete",{event_id:t.event_id,status:t.status,msg:t.msg,code:t.code})}:{name:e,args:t}}function l(e){const t=String(e??"").trim();return t?t.match(I)?.[1]:void 0}function u(e){const t=String(e??"").trim();if(!t)return;const n=l(t);if(n)return _(n);const i=t.match(/(?:chat_id|session_id)\s*=\s*"([A-Za-z0-9._-]+)"/)?.[1];if(i)return i;const o=t.match(/[A-Za-z0-9._-]+/g)??[];for(const s of o)if(!(s==="event_id"||s==="chat_id"||s==="session_id")&&s.length>0)return s;return t.match(S)?.[0]}function _(e){if(!e)return;const t=e.split(":",1)[0]?.trim();if(t)return u(t)}function m(e,t){if(e!=="grix_reply"&&e!=="grix_complete")return t;const n={...t},i=l(n.event_id);if(i&&(n.event_id=i),e==="grix_reply"){const o=_(i),d=String(n.session_id??""),s=u(n.session_id),b=/\bevent_id\b|["'<>\s]/.test(d);s&&!(b&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function V(e){return c.has(e)}function X(e,t){switch(e){case"grix_query":return T(t);case"grix_audit_data":return M(t);case"grix_group":return O(t);case"grix_message_send":return D(t);case"grix_message_unsend":return L(t);case"grix_file_link":return E(t);case"grix_file_upload":return j(t);case"grix_admin":return $(t);case"grix_call_owner":return N(t);case"grix_agent_update":return P(t);case"grix_dispatch_agent":return C(t);case"grix_session_send":return U(t);case"grix_chat_state_query":return z(t);case"grix_chat_state_update":return B(t);case"grix_access_control":return R(t);case"grix_widget_visitor_ban":return G(t);case"grix_skill_set":return W(t);case"grix_skill_get":return F(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",search_favorite_sessions:"search_favorite_sessions"};function T(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.sessionType!=null&&(i.session_type=e.sessionType),e.limit!=null?i.limit=e.limit:t==="message_history"&&(i.limit=1),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}const y={get_manifest:"audit_get_manifest",list_spans:"audit_list_spans",get_content_chunk:"audit_get_content_chunk"};function M(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_audit_data action: ${t}`);const i={audit_id:e.auditId};return e.revision!=null&&(i.revision=e.revision),e.cursor!=null&&(i.cursor=e.cursor),e.limit!=null&&(i.limit=e.limit),e.contentId!=null&&(i.content_id=e.contentId),e.maxBytes!=null&&(i.max_bytes=e.maxBytes),{action:n,params:i}}const h={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 O(e){const t=String(e.action??""),n=h[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 D(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 L(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function E(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}function j(e){const t={file_path:e.file_path,session_id:e.session_id};return e.caption!=null&&(t.caption=e.caption),e.reply_to_message_id!=null&&(t.reply_to_message_id=e.reply_to_message_id),{action:"file_upload",params:t,timeoutMs:6e4}}const f={create_agent:"agent_api_create",list_categories:"agent_category_list",create_category:"agent_category_create",update_category:"agent_category_update",assign_category:"agent_category_assign",rotate_api_key:"agent_api_key_rotate"};function N(e){return{action:"call_owner",params:{session_id:e.session_id}}}function P(e){if(e.agent_name==null&&e.introduction==null)throw new Error("grix_agent_update requires agent_name or introduction");const t={agent_id:e.agent_id};return e.agent_name!=null&&(t.agent_name=e.agent_name),e.introduction!=null&&(t.introduction=e.introduction),{action:"agent_introduction_update",params:t}}function C(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task,title:e.title},timeoutMs:75e3}}function U(e){const t={session_id:e.session_id,content:e.content};return e.quoted_message_id!=null&&String(e.quoted_message_id).trim()!==""&&(t.quoted_message_id=e.quoted_message_id),{action:"session_send",params:t}}function z(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 R(e){const t=String(e.action??""),n=Q[t];if(!n)throw new Error(`Unknown grix_access_control action: ${t}`);const i={};return e.code!=null&&(i.code=e.code),e.sender_id!=null&&(i.sender_id=e.sender_id),e.policy!=null&&(i.policy=e.policy),{action:"claude_access_control",params:{verb:n,payload:i},timeoutMs:3e4}}function G(e){return{action:"widget_visitor_ban",params:{session_id:e.session_id}}}function B(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 W(e){return{action:"skill_set",params:{name:e.name,content:e.content},timeoutMs:3e4}}function F(e){const t={};return e.name!=null&&(t.name=e.name),{action:"skill_get",params:t}}function $(e){const t=String(e.action??""),n=f[t];if(!n)throw new Error(`Unknown grix_admin action: ${t}`);const i={};return e.agentName!=null&&(i.agent_name=e.agentName),e.introduction!=null&&(i.introduction=e.introduction),e.isMain!=null&&(i.is_main=e.isMain),e.agentId!=null&&(i.agent_id=e.agentId),e.categoryId!=null&&(i.category_id=e.categoryId),e.name!=null&&(i.name=e.name),e.parentId!=null&&(i.parent_id=e.parentId),e.sortOrder!=null&&(i.sort_order=e.sortOrder),{action:n,params:i}}const Y=new Set([...Object.values(g),...Object.values(y),...Object.values(h),...Object.values(f),"send_msg","delete_msg","file_link","file_upload","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update","claude_access_control","widget_visitor_ban","skill_set","skill_get"]),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,q as ALL_TOOLS,a as EVENT_TOOLS,A as EXPOSED_TOOLS,Y as PHASE1_INVOKE_ACTIONS,x as PHASE1_TOOL_NAMES,k as PHASE2_TOOL_NAMES,r as TOOLS,p as TOOL_ALIASES,Z as TOOL_MAP,V as isAlias,H as isGrixInternalToolName,K as mapToolAlias,m as normalizeEventToolArgs,X as toolCallToInvoke};
1
+ const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},keyword:{type:"string",description:"Search keyword."},sessionType:{type:"integer",enum:[1,2],description:"Filter by session type (session_search only): 1=private chat, 2=group chat. Omit to return all."},limit:{type:"integer",description:"Max results. message_history defaults to 1 when omitted."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"]},id:{type:"string"},keyword:{type:"string",maxLength:200},sessionType:{type:"integer",enum:[1,2]},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_audit_data",description:"Read connector-backed conversation audit replay data from Grix/AIBot: manifest, span pages, and content chunks.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"],description:"Audit read action. Start with get_manifest, then call list_spans and/or get_content_chunk based on the manifest."},auditId:{type:"string",description:"Audit ID returned by the audited turn metadata."},revision:{type:"integer",description:"Optional manifest revision to read. Usually use the revision returned by get_manifest."},cursor:{type:"string",description:"Opaque pagination cursor returned by list_spans or get_content_chunk."},limit:{type:"integer",description:"Max span items for list_spans."},contentId:{type:"string",description:"Content reference ID from manifest.content_refs (get_content_chunk only)."},maxBytes:{type:"integer",description:"Max UTF-8 bytes to return for get_content_chunk."}},required:["action","auditId"]},validation:{required:["action","auditId"],properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"]},auditId:{type:"string"},revision:{type:"integer",minimum:1},cursor:{type:"string",maxLength:4096},limit:{type:"integer",minimum:1,maximum:200},contentId:{type:"string"},maxBytes:{type:"integer",minimum:1,maximum:131072}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform. Do NOT use this to answer the inbound event you are currently handling \u2014 answer in plain text (or the reply tool where available) instead, otherwise the user receives duplicate messages.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. The link is reachable only inside the tailnet, so just send it as-is \u2014 no need to worry about or mention any link lifetime. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds. Leave unset to use the long default; set only if you deliberately want a short-lived link."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_file_upload",description:"Upload a local file to the Grix platform and send it as a media message in the target session. Supports images (jpg/png/webp/gif/bmp/heic/heif), videos (mp4/mov/m4v/webm/mkv/avi), documents (pdf/doc/docx/xls/xlsx/ppt/pptx/txt/md/csv/json/xml), and archives (zip/rar/7z/tar/gz). Max 50 MB. Use this instead of grix_file_link when the file should appear as a native attachment in the chat (visible inline for images/videos), rather than a tailnet download link.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file to upload."},session_id:{type:"string",description:"Target session ID to send the file to."},caption:{type:"string",description:"Optional text caption for the media message."},reply_to_message_id:{type:"string",description:"Optional message ID to quote/reply to."}},required:["file_path","session_id"]},validation:{required:["file_path","session_id"],properties:{file_path:{type:"string",maxLength:4096},session_id:{type:"string"},caption:{type:"string",maxLength:2e3},reply_to_message_id:{type:"string"}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Professional behavioral introduction describing the Agent purpose, responsibilities, operating expectations, and boundaries (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the display name and/or text introduction of one of your owner's agents, identified by its numeric agent ID. Provide agent_name, introduction, or both \u2014 at least one is required.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},agent_name:{type:"string",description:"New display name (max 100 characters, must be unique among the owner's agents)."},introduction:{type:"string",description:"New text introduction (max 300 characters)."}},required:["agent_id"]},validation:{required:["agent_id"],properties:{agent_id:{type:"string"},agent_name:{type:"string",maxLength:100},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:`Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend creates a NEW private session between the owner and that agent for each dispatch (it does not reuse past sessions), binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session AS THE OWNER so the agent starts working. Because the task is delivered as the owner, write it in the owner's first-person voice and tone \u2014 phrase it the way the owner would speak directly to the agent (e.g. "\u5E2E\u6211\u2026", "\u4F60\u53BB\u2026"), NOT as a third-person relay or as yourself narrating on the owner's behalf. Provide a short title summarizing the core of the task \u2014 it becomes the new session's title; if omitted the backend derives one from the task text.`,inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},cwd:{type:"string",description:"Absolute working directory where the agent should do the work."},task:{type:"string",description:"Text description of the task to perform, written in the owner's first-person voice and tone \u2014 it is delivered into the session as the owner, so phrase it as the owner speaking directly to the agent, not as a third-person relay."},title:{type:"string",description:"Optional short title (a few words) summarizing the core of the task; used as the new session's title. If omitted, the backend derives a title from the task text."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4},title:{type:"string",maxLength:255}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Dispatch callbacks (report_dispatch_result) MUST use this tool, even when you are a member of the callback session: a message sent as yourself cannot quote-wake the dispatcher agent, and your membership is not checked. NEVER use this for your own ordinary replies in a conversation \u2014 that would make your words show up as the owner's message; reply normally (or use grix_message_send to send as yourself) for those. The owner must be a member of the target session. Requires the Send as Owner permission scope \u2014 if rejected for missing permission, surface the error as-is so the owner can grant it; do not fall back to grix_message_send. Optional quoted_message_id quotes a message in the same session (used by dispatch callbacks to wake the dispatcher agent).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."},quoted_message_id:{type:"string",description:"Optional message ID in the target session to quote/reply to.",pattern:"^[1-9]\\d*$"}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4},quoted_message_id:{type:"string",pattern:"^[1-9]\\d*$"}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). When session_id identifies one completed session, the entry also includes exactly one clean final_result; non-completed and list queries never include message content. Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_widget_visitor_ban",description:"Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Widget visitor session ID (required)."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_egg_search",description:"Search the Grix egg market (published skill/persona packages the owner can hatch into a new agent or install into an existing one). Keyword matching is per-term AND over name + description + category, so search short keywords (one concept per call) rather than a whole sentence; omit keyword to browse a category. Results include id, name, description, category, install_count, can_create_agent, and existing_agent_client_types.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Short search keyword(s); terms are ANDed. Omit to list by category."},categoryId:{type:"string",description:"Optional egg category ID to restrict results."},locale:{type:"string",description:"Optional locale such as zh-CN or en; defaults to the owner's locale."},page:{type:"integer",description:"Page number, starting from 1."},pageSize:{type:"integer",description:"Items per page (1-50), default 20."}}},validation:{required:[],properties:{keyword:{type:"string",maxLength:200},categoryId:{type:"string"},locale:{type:"string",maxLength:16},page:{type:"integer",minimum:1},pageSize:{type:"integer",minimum:1,maximum:50}}}},{name:"grix_egg_get",description:"Get one Grix egg by id \u2014 full description, current version, and whether it can create a new agent or install into existing agents. Use after grix_egg_search to show details before the owner decides to hatch.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Egg ID from grix_egg_search."},locale:{type:"string",description:"Optional locale."},version:{type:"integer",description:"Optional specific version; defaults to the current one."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"},locale:{type:"string",maxLength:16},version:{type:"integer",minimum:1}}}},{name:"grix_skill_set",description:"Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (max 100 characters); unique within the owner. Used as the identifier and the synced directory name."},content:{type:"string",description:"Full SKILL.md text. Pass an empty string to delete the skill by name."}},required:["name","content"]},validation:{required:["name","content"],properties:{name:{type:"string",maxLength:100},content:{type:"string",maxLength:262144}}}},{name:"grix_skill_get",description:"Read one of your owner's custom skills by name (returns its full SKILL.md content), or list the owner's skill library when no name is given. Use this before editing to fetch current content, then call grix_skill_set with the revised text.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name to fetch. Omit to list all skills (names + versions) instead."}}},validation:{required:[],properties:{name:{type:"string",maxLength:100}}}}],a=[{name:"grix_reply",description:"Send your final reply \u2014 the conclusion the user is waiting for \u2014 to the specified session. This is the message the user treats as your answer; deliver your final result here, and use plain text only for brief progress notes while you work. Supports streaming in chunks; the frontend automatically aggregates them into one complete message. The connector quotes the message being answered automatically, so quoted_message_id is optional (set it only to quote a different earlier message).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],b={grix_dispatch_agent:"grix-agent-dispatch",grix_agent_update:"grix-agent-dispatch",grix_query:"grix-query",grix_audit_data:"grix-audit-data",grix_group:"grix-group",grix_message_send:"message-send",grix_message_unsend:"message-unsend",grix_admin:"grix-admin",grix_call_owner:"grix-owner-relay",grix_session_send:"grix-owner-relay",grix_chat_state_query:"grix-chat-state",grix_chat_state_update:"grix-chat-state",grix_status:"grix-chat-state",grix_access_control:"grix-access-control",grix_widget_visitor_ban:"grix-widget-visitor-ban",grix_file_link:"tailnet-file-share"};function w(e){return` Before calling this tool, follow the \`${e}\` skill's procedure first; do not invoke this tool directly without going through that skill's guidance.`}for(const e of[...r,...a]){const t=b[e.name];t&&(e.description+=w(t))}const p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),x=new Set(r.map(e=>e.name)),k=new Set(a.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,S=/[A-Za-z0-9._-]+/;function Z(e){return!!(x.has(e)||k.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const q=[...r,...a],A=[...q,...p],V=new Map(A.map(e=>[e.name,e]));function X(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(S)?.[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),d=String(n.session_id??""),s=u(n.session_id),v=/\bevent_id\b|["'<>\s]/.test(d);s&&!(v&&o)?n.session_id=s:o&&(n.session_id=o)}return n}function Y(e){return c.has(e)}function J(e,t){switch(e){case"grix_query":return T(t);case"grix_audit_data":return D(t);case"grix_group":return L(t);case"grix_message_send":return E(t);case"grix_message_unsend":return j(t);case"grix_file_link":return N(t);case"grix_file_upload":return z(t);case"grix_admin":return H(t);case"grix_call_owner":return P(t);case"grix_agent_update":return C(t);case"grix_dispatch_agent":return U(t);case"grix_session_send":return G(t);case"grix_chat_state_query":return R(t);case"grix_chat_state_update":return F(t);case"grix_access_control":return B(t);case"grix_widget_visitor_ban":return W(t);case"grix_egg_search":return O(t);case"grix_egg_get":return M(t);case"grix_skill_set":return $(t);case"grix_skill_get":return Q(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",search_favorite_sessions:"search_favorite_sessions"};function T(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.sessionType!=null&&(i.session_type=e.sessionType),e.limit!=null?i.limit=e.limit:t==="message_history"&&(i.limit=1),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}function O(e){const t={};return e.keyword!=null&&(t.keyword=e.keyword),e.categoryId!=null&&(t.category_id=e.categoryId),e.locale!=null&&(t.locale=e.locale),e.page!=null&&(t.page=e.page),e.pageSize!=null&&(t.page_size=e.pageSize),{action:"egg_search",params:t}}function M(e){const t={id:e.id};return e.locale!=null&&(t.locale=e.locale),e.version!=null&&(t.version=e.version),{action:"egg_get",params:t}}const y={get_manifest:"audit_get_manifest",list_spans:"audit_list_spans",get_content_chunk:"audit_get_content_chunk"};function D(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_audit_data action: ${t}`);const i={audit_id:e.auditId};return e.revision!=null&&(i.revision=e.revision),e.cursor!=null&&(i.cursor=e.cursor),e.limit!=null&&(i.limit=e.limit),e.contentId!=null&&(i.content_id=e.contentId),e.maxBytes!=null&&(i.max_bytes=e.maxBytes),{action:n,params:i}}const h={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function L(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function E(e){const t={session_id:e.sessionId,msg_type:e.msgType??1,content:e.content};return e.quotedMessageId!=null&&(t.quoted_message_id=e.quotedMessageId),e.threadId!=null&&(t.thread_id=e.threadId),{action:"send_msg",params:t}}function j(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function N(e){const t={file_path:e.file_path};return e.ttl_ms!=null&&(t.ttl_ms=e.ttl_ms),{action:"file_link",params:t}}function z(e){const t={file_path:e.file_path,session_id:e.session_id};return e.caption!=null&&(t.caption=e.caption),e.reply_to_message_id!=null&&(t.reply_to_message_id=e.reply_to_message_id),{action:"file_upload",params:t,timeoutMs:6e4}}const f={create_agent:"agent_api_create",list_categories:"agent_category_list",create_category:"agent_category_create",update_category:"agent_category_update",assign_category:"agent_category_assign",rotate_api_key:"agent_api_key_rotate"};function P(e){return{action:"call_owner",params:{session_id:e.session_id}}}function C(e){if(e.agent_name==null&&e.introduction==null)throw new Error("grix_agent_update requires agent_name or introduction");const t={agent_id:e.agent_id};return e.agent_name!=null&&(t.agent_name=e.agent_name),e.introduction!=null&&(t.introduction=e.introduction),{action:"agent_introduction_update",params:t}}function U(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task,title:e.title},timeoutMs:75e3}}function G(e){const t={session_id:e.session_id,content:e.content};return e.quoted_message_id!=null&&String(e.quoted_message_id).trim()!==""&&(t.quoted_message_id=e.quoted_message_id),{action:"session_send",params:t}}function R(e){const t={};return e.session_id!=null&&(t.session_id=e.session_id),e.page!=null&&(t.page=e.page),e.page_size!=null&&(t.page_size=e.page_size),e.state!=null&&(t.state=e.state),{action:"chat_state_query",params:t}}function B(e){const t=String(e.action??""),n=K[t];if(!n)throw new Error(`Unknown grix_access_control action: ${t}`);const i={};return e.code!=null&&(i.code=e.code),e.sender_id!=null&&(i.sender_id=e.sender_id),e.policy!=null&&(i.policy=e.policy),{action:"claude_access_control",params:{verb:n,payload:i},timeoutMs:3e4}}function W(e){return{action:"widget_visitor_ban",params:{session_id:e.session_id}}}function F(e){const t={session_id:e.session_id,state:e.state};return e.reason!=null&&(t.reason=e.reason),{action:"chat_state_update",params:t}}function $(e){return{action:"skill_set",params:{name:e.name,content:e.content},timeoutMs:3e4}}function Q(e){const t={};return e.name!=null&&(t.name=e.name),{action:"skill_get",params:t}}function H(e){const t=String(e.action??""),n=f[t];if(!n)throw new Error(`Unknown grix_admin action: ${t}`);const i={};return e.agentName!=null&&(i.agent_name=e.agentName),e.introduction!=null&&(i.introduction=e.introduction),e.isMain!=null&&(i.is_main=e.isMain),e.agentId!=null&&(i.agent_id=e.agentId),e.categoryId!=null&&(i.category_id=e.categoryId),e.name!=null&&(i.name=e.name),e.parentId!=null&&(i.parent_id=e.parentId),e.sortOrder!=null&&(i.sort_order=e.sortOrder),{action:n,params:i}}const ee=new Set([...Object.values(g),...Object.values(y),...Object.values(h),...Object.values(f),"send_msg","delete_msg","file_link","file_upload","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update","claude_access_control","widget_visitor_ban","skill_set","skill_get"]),K={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{K as ACCESS_CONTROL_ACTION_MAP,q as ALL_TOOLS,a as EVENT_TOOLS,A as EXPOSED_TOOLS,ee as PHASE1_INVOKE_ACTIONS,x as PHASE1_TOOL_NAMES,k as PHASE2_TOOL_NAMES,r as TOOLS,p as TOOL_ALIASES,V as TOOL_MAP,Y as isAlias,Z as isGrixInternalToolName,X as mapToolAlias,_ as normalizeEventToolArgs,J as toolCallToInvoke};
@@ -1 +1 @@
1
- import{existsSync as v,mkdirSync as K,readFileSync as S,renameSync as X,writeFileSync as J}from"node:fs";import{join as g,dirname as Y}from"node:path";import{spawn as D}from"node:child_process";import{log as f}from"../log/index.js";import{GRIX_PATHS as _}from"../log/index.js";import{resolveClientVersion as R}from"../util/client-version.js";import{UpgradeError as T,armWindowsUpgradeHandoff as U,armWindowsUpgradeRollback as Z,cleanupPendingBackup as $,collectEnvInfo as Q,discardPendingStaging as N,finalizePendingTransaction as P,getUpgradeLogTail as ee,isVersionAvailable as te,npmInstall as ae,normalizeUpgradeVersion as re,pendingExists as O,preflightCheck as ie,readPending as k,readPendingPackageVersion as ne,removeWindowsHandoffFiles as se,rollbackPendingInstall as oe,updatePending as E,upgradeLog as n,verifyInstalledVersion as de,writePending as ce}from"./npm-upgrader.js";import{isRetriableRegistryError as le,isTransientInstallError as ue}from"../installer/npm-registry.js";const pe=360*60*1e3,he=300*1e3,G=1800*1e3,fe=2,ge=3,me=10,M="grix-connector",W=1e4,L=15,_e=2e3,we=8e3,ve=1e3,ye=2e3,be=120*1e3,C=1e4,Ae=6e4,Se=3,Te=600*1e3,Pe=new Set(["preparing","staged","handoff_ready","activating","verifying","healthy","rolling_back","rolled_back","finalizing","failed"]),x=600*1e3,I=1800*1e3;function q(s,e=x){const t=Date.parse(s.activation_started_at??s.upgraded_at??"");return Number.isNaN(t)?!0:Date.now()-t>e}function V(s){const e=Date.parse(s.verification_started_at??s.activation_started_at??"");return Number.isNaN(e)||Date.now()-e>be}function Re(s){if((s.guardian_handback_count??0)>=Se)return!1;const e=Date.parse(s.guardian_handback_at??"");return Number.isNaN(e)?!0:Date.now()-e>=Te}function ke(s){if(!Number.isInteger(s)||(s??0)<=0)return!1;try{return process.kill(s,0),!0}catch{return!1}}function A(){const s=g(_.base,"bin"),e=g(s,"upgrade-guardian.mjs");if(v(e))return e;const t=g(s,"upgrade-guardian.fallback.mjs");return v(t)?t:null}function H(s){if(!ke(s.guardian_pid))return!1;try{const t=JSON.parse(S(g(_.data,"upgrade-monitor-heartbeat.json"),"utf-8"));if(t.transaction_id===s.transaction_id&&t.pid===s.guardian_pid&&typeof t.updated_at=="number"&&Date.now()-t.updated_at<=C)return!0}catch{}const e=Date.parse(s.guardian_started_at??"");return Number.isFinite(e)&&Date.now()-e<=C}function B(s){return new URL(s.replace(/^wss:/,"https:").replace(/^ws:/,"http:")).origin}function F(){return g(_.data,"upgrade-state.json")}function j(){const s=F();if(!v(s))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(S(s,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function Ee(s){const e=F();K(Y(e),{recursive:!0});const t=e+".tmp";J(t,JSON.stringify(s),"utf-8"),X(t,e)}function z(){return new Date().toISOString().slice(0,10)}class Ce{agentConfigs;isBusy;closeAdmission;options;timer=null;initialTimer=null;pendingTimer=null;guardianWaitLoggedAt=0;guardianWaitLoggedPhase="";recordedRollbackFailures=new Set;posixGuardianResumeAttempted=new Set;running=!1;stopped=!1;constructor(e,t,i,a={}){this.agentConfigs=e,this.isBusy=t,this.closeAdmission=i,this.options=a}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),pe)))},he)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null),this.pendingTimer&&(clearTimeout(this.pendingTimer),this.pendingTimer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(i=>this.queryUpgrade(i)))).find(i=>i.available&&i.release)??{available:!1}}async handlePendingOnStartup(){if(!O())return;let e=k();if(!e){n("daemon startup: pending journal is unreadable; preserving it and blocking automatic upgrade for manual recovery");return}if(e.phase&&!Pe.has(e.phase)&&(e=E({phase:"failed",error_code:"INVALID_UPGRADE_STATE",error_msg:`Unknown upgrade transaction phase: ${String(e.phase)}`},e.transaction_id)),e.phase==="finalizing"){P(e)||(n(`terminal transaction ${e.transaction_id??"legacy"} is waiting for its shared lock to be released`),this.schedulePendingReconcile());return}if(e.phase==="preparing"||e.phase==="staged"){const o=!!(e.wrapper_pause_file&&v(e.wrapper_pause_file));if(e.phase==="staged"&&o){this.schedulePendingReconcile();return}N(e),e=E({phase:"rolled_back",error_code:"UPGRADE_INTERRUPTED",error_msg:"Upgrade stopped before Windows activation started"})}if(e.phase==="verifying"){if(H(e)&&!V(e)){this.schedulePendingReconcile();return}if(V(e)&&e.transaction_id&&["win32","darwin"].includes(this.options.platform??process.platform)&&this.options.requestGracefulShutdown){const o=`Target ${e.target_version} did not become healthy before the Windows verification deadline`;Z(e.transaction_id,o),n(`${o}; requesting graceful shutdown for offline rollback`),await this.options.requestGracefulShutdown("upgrade");return}await this.resumeWindowsGuardian(),this.schedulePendingReconcile();return}const t=new Set(["handoff_ready","activating","rolling_back"]);if(e.phase&&t.has(e.phase)){if(H(e)){this.logGuardianWait(e),this.schedulePendingReconcile();return}if(q(e)&&e.transaction_id&&(this.options.platform??process.platform)==="win32"&&this.options.requestGracefulShutdown&&Re(e)){const o=`Guardian did not settle ${e.phase} for ${e.target_version} before the Windows activation deadline`;E({guardian_handback_count:(e.guardian_handback_count??0)+1,guardian_handback_at:new Date().toISOString()},e.transaction_id),n(`${o}; requesting graceful shutdown so the wrapper re-runs the guardian`),await this.options.requestGracefulShutdown("upgrade");return}await this.resumeWindowsGuardian(),this.logGuardianWait(e),this.schedulePendingReconcile();return}if(e.phase==="failed"){n(`daemon startup: upgrade transaction requires manual recovery: ${e.error_msg??e.error_code??"unknown error"}`);return}const i=ne(e),a=R(),r=e.phase==="healthy",d=e.phase==="rolled_back",h=!i||i===e.target_version,c=r?a===e.target_version&&i===e.target_version:!d&&a===e.target_version&&h;if(!c&&h&&!d){if(!q(e)){if((this.options.platform??process.platform)!=="win32"&&i===e.target_version){const o=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.posixGuardianResumeAttempted.has(o)||(this.posixGuardianResumeAttempted.add(o),n(`resuming POSIX guardian for stalled activation of ${e.target_version}`),this.startGuardian())}this.logThrottled("posix-activation-wait",`daemon startup: runtime ${a} is waiting for guardian activation of ${e.target_version} (active package=${i??"unknown"})`),this.schedulePendingReconcile();return}n(`daemon startup: activation wait for ${e.target_version} expired (> ${x/6e4}min); no guardian will finish the switch, reporting by version verdict`)}const u=c?{from_version:e.from_version,to_version:e.target_version,status:"success"}:{from_version:e.from_version,to_version:e.target_version,status:"rolled_back",error_code:e.error_code??"STARTUP_CRASH",error_msg:e.error_msg,crash_count:e.crash_count};if(n(c?`daemon startup: version matches target ${e.target_version}, upgrade succeeded`:`daemon startup: runtime=${a}, active package=${i??"unknown"}, target=${e.target_version}; rolled back`),!c){const o=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.recordedRollbackFailures.has(o)||(this.recordedRollbackFailures.add(o),this.recordFailure(e.target_version))}if(c){const o=await this.confirmHealthz(we,r?e.target_version:void 0,r?e.old_pid:void 0,r?e.activation_started_at:void 0);if(r&&!o){n("guardian marked healthy but target-version healthz is not yet observable; retrying reconciliation"),this.schedulePendingReconcile();return}n(o?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match (guardian still guards crashes)")}if(!$(e)){n(`terminal transaction ${e.transaction_id??"legacy"} is waiting for antivirus-held artifact cleanup`),this.schedulePendingReconcile();return}if(await this.reportUpgrade(u)){P(e)||(n(`terminal transaction ${e.transaction_id??"legacy"} could not release its shared lock; retrying reconciliation`),this.schedulePendingReconcile());return}this.retryStartupReport(u,e,!1)}schedulePendingReconcile(){this.stopped||this.pendingTimer||(this.pendingTimer=setTimeout(()=>{this.pendingTimer=null,!this.stopped&&this.handlePendingOnStartup().catch(e=>{f.warn("upgrade",`Pending reconciliation failed: ${e instanceof Error?e.message:e}`),this.schedulePendingReconcile()})},ye))}logThrottled(e,t){const i=Date.now();e===this.guardianWaitLoggedPhase&&i-this.guardianWaitLoggedAt<Ae||(this.guardianWaitLoggedPhase=e,this.guardianWaitLoggedAt=i,n(t))}logGuardianWait(e){const t=String(e.phase??"");this.logThrottled(`guardian:${t}`,`transaction ${e.transaction_id??"legacy"} is ${t}; waiting for guardian verdict`)}async resumeWindowsGuardian(){if(this.options.resumeWindowsGuardian){await this.options.resumeWindowsGuardian();return}const e=A();if(!e)return;D(process.execPath,[e,"activate","--root",_.base],{detached:!0,stdio:"ignore",windowsHide:!0}).unref()}async retryStartupReport(e,t,i){const a=t?.transaction_id,r=()=>{const d=k();if(!(a&&d?.transaction_id!==a)){if(i&&!$(t)){n(`terminal backup cleanup is still blocked for ${a??"legacy transaction"}`),this.schedulePendingReconcile();return}P(t)||(n(`terminal shared lock release is still blocked for ${a??"legacy transaction"}`),this.schedulePendingReconcile())}};for(let d=2;d<=L;d++){if(this.stopped)return;const h=_e*(d-1)+Math.floor(Math.random()*1e3);if(await new Promise(c=>setTimeout(c,h)),this.stopped)return;if(await this.reportUpgrade(e)){r();return}}n(`startup report (${e.status}) not delivered after ${L} attempts; clearing pending to unblock future checks`),r()}async confirmHealthz(e,t,i,a){let r="19579";try{const c=g(_.data,"health-port");v(c)&&(r=S(c,"utf-8").trim()||r)}catch{}const d=`http://127.0.0.1:${r}/healthz`,h=Date.now()+e;for(;Date.now()<h&&!this.stopped;){try{const c=await fetch(d,{signal:AbortSignal.timeout(2e3)});if(c.ok){if(!t)return!0;const u=await c.json();if(u.status==="ok"&&u.version===t&&typeof u.pid=="number"&&u.pid>0&&u.pid!==i)try{const o=JSON.parse(S(g(_.base,"daemon-status.json"),"utf-8")),y=Date.parse(a??"");return!!(o.state==="running"&&o.pid===u.pid&&typeof o.updated_at=="number"&&Number.isFinite(y)&&o.updated_at>=y)}catch{return!1}}}catch{}await new Promise(c=>setTimeout(c,ve))}return!1}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(e){f.error("upgrade",`Check failed: ${e instanceof Error?e.message:e}`)}finally{this.running=!1}}}async check(){if(O()){await this.handlePendingOnStartup();return}if(this.agentConfigs.length===0)return;const i=(await Promise.all(this.agentConfigs.map(l=>this.queryUpgrade(l)))).find(l=>l.available&&l.release)?.release;if(!i)return;const a=R();let r;try{r=re(i.version)}catch(l){const p=l;await this.reportUpgrade({from_version:a,to_version:String(i.version),status:"failed",error_code:p.code??"INVALID_VERSION",error_msg:p.message});return}const d=i.force,h=this.options.platform??process.platform,c=this.options.env??process.env;if(h==="win32"){const l=A(),p=c.GRIX_WRAPPER_SUPERVISED==="1";if(!p||!l||!this.options.requestGracefulShutdown){const m=`Windows automatic upgrade requires a refreshed service installation (${[...p?[]:["managed wrapper"],...l?[]:["external guardian"],...this.options.requestGracefulShutdown?[]:["graceful shutdown hook"]].join(", ")})`;n(m),await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:"WINDOWS_UPGRADE_UNSUPPORTED",error_msg:m});return}}const u=h==="darwin"&&!!A()&&!!this.options.requestGracefulShutdown;if(h==="darwin"&&!u&&n("macOS transactional upgrade prerequisites missing; falling back to the legacy guardian flow"),!d&&!this.checkRateLimit(r))return;const o=ie();if(!o.ok){await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:o.errorCode,error_msg:o.errorMsg});return}if(!await te(M,r)){n(`target ${r} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}n(`upgrade start: ${a} -> ${r}`);const y=Date.now();let w;try{w=ce(a,r,h==="win32"||u?"preparing":void 0).transaction_id,(await ae(M,r,void 0,u?{activationMode:"staged"}:{}))?.activationRequired||de(r),n("npm install verified, reporting installed"),await this.reportUpgrade({from_version:a,to_version:r,status:"installed",duration_ms:Date.now()-y}),n("shutting down for restart");const p=await this.waitUntilIdle();if(p!=="idle")throw n(`upgrade aborted before lifecycle handoff: ${p}`),this.options.reopenLifecycleAdmission?.(),new T("UPGRADE_ABORTED",`Upgrade was aborted before lifecycle handoff (${p})`);if(h==="win32"){if(!A())throw new T("WINDOWS_UPGRADE_UNSUPPORTED","Windows external guardian disappeared before lifecycle handoff");U(process.pid,w),n("Windows handoff armed; requesting graceful daemon shutdown"),await this.options.requestGracefulShutdown("upgrade")}else if(u){if(!A())throw new T("MACOS_UPGRADE_UNSUPPORTED","macOS guardian helper disappeared before lifecycle handoff");U(process.pid,w),n("macOS handoff armed; spawning guardian before graceful daemon shutdown"),await this.resumeWindowsGuardian(),await this.options.requestGracefulShutdown("upgrade")}else this.startGuardian(),process.kill(process.pid,"SIGTERM")}catch(l){const p=l instanceof T?l.code:"NPM_INSTALL_FAILED",b=l instanceof Error?l.message:String(l);n(`upgrade failed: ${p} ${b}`);const m=k();if(w&&m?.transaction_id===w&&(h==="win32"||u?(se(),N(m)):oe(m)&&n("upgrade failed before restart; previous package restored atomically"),P(m)||(n(`failed transaction ${w} is waiting for shared lock release`),this.schedulePendingReconcile()),this.options.reopenLifecycleAdmission?.()),p==="UPGRADE_IN_PROGRESS")return;le(b)||ue(b)||p==="NPM_TIMEOUT"?n(`failure is retriable (${p}); skipping cooldown, will retry next cycle`):this.recordFailure(r),await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:p,error_msg:b,duration_ms:Date.now()-y,upgrade_log:ee()})}}async waitUntilIdle(){if(this.isBusy?.()){n(`active tasks detected, waiting up to ${Math.round(I/6e4)}min until idle before restart (admission stays open while waiting)`);const e=Date.now()+I,t=5e3,i=600*1e3;let a=Date.now();for(;this.isBusy()&&!this.stopped;){if(Date.now()>=e)return f.warn("upgrade",`Idle wait exceeded ${Math.round(I/6e4)}min; abandoning this upgrade attempt. Admission was never closed, so agents kept serving. A busy state that never clears usually means an adapter leaked its busy flag \u2014 check for a turn that never completed.`),n("idle wait timed out; upgrade attempt abandoned without ever closing admission"),"timeout";await new Promise(r=>setTimeout(r,t)),Date.now()-a>=i&&(a=Date.now(),n("still waiting for active tasks to finish before restart"))}}return this.stopped?"stopped":(this.closeAdmission?.(),this.isBusy?.()?(n("work arrived while closing admission; reopening and abandoning this upgrade attempt"),this.options.reopenLifecycleAdmission?.(),"busy-again"):(n("all tasks completed, proceeding with restart"),"idle"))}checkRateLimit(e){const t=j();if(t.last_failure_at){const d=Date.now()-new Date(t.last_failure_at).getTime();if(d<G)return f.info("upgrade",`In cooldown, ${(G-d)/6e4}m remaining`),!1}const i=t.version_attempts[e]??0;if(i>=ge)return f.info("upgrade",`Version ${e} already tried ${i} times, skipping`),!1;const a=z(),r=t.daily_attempts[a]??0;return r>=fe?(f.info("upgrade",`Already ${r} attempts today, skipping`),!1):!0}recordFailure(e){const t=j(),i=z();t.last_failure_at=new Date().toISOString(),t.last_failure_version=e,t.daily_attempts[i]=(t.daily_attempts[i]??0)+1,t.version_attempts[e]=(t.version_attempts[e]??0)+1;for(const r of Object.keys(t.daily_attempts))r<i&&delete t.daily_attempts[r];const a=Object.keys(t.version_attempts);for(const r of a.slice(0,Math.max(0,a.length-me)))r!==e&&delete t.version_attempts[r];Ee(t)}async queryUpgrade(e){const i=`${B(e.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:R(),channel:e.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const a=await fetch(i,{headers:{Authorization:`Bearer ${e.apiKey}`},signal:AbortSignal.timeout(W)});if(!a.ok)return f.warn("upgrade",`Check API returned ${a.status}`),{available:!1};const r=await a.json();return r.code!==0?{available:!1}:r.data}catch(a){return f.warn("upgrade",`Check API error: ${a instanceof Error?a.message:a}`),{available:!1}}}async reportUpgrade(e){if(this.agentConfigs.length===0)return!1;const t={client_type:"grix-connector",...e.npm_version?e:{...e,...Q()}};return(await Promise.all(this.agentConfigs.map(async a=>{const d=`${B(a.wsUrl)}/v1/agent-api/upgrade/report`;try{return(await fetch(d,{method:"POST",headers:{Authorization:`Bearer ${a.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:AbortSignal.timeout(W)})).ok}catch{return!1}}))).some(a=>a)}startGuardian(){const e=g(_.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!v(e)){f.info("upgrade","Guardian not available on this platform, skipping");return}try{const t=D(e,[],{detached:!0,stdio:"ignore"});t.unref(),n(`guardian started (pid ${t.pid})`)}catch(t){f.warn("upgrade",`Failed to start guardian: ${t instanceof Error?t.message:t}`)}}}export{Ce as UpgradeChecker};
1
+ import{existsSync as v,mkdirSync as X,readFileSync as S,renameSync as J,writeFileSync as Y}from"node:fs";import{join as g,dirname as Z}from"node:path";import{spawn as U}from"node:child_process";import{log as f}from"../log/index.js";import{GRIX_PATHS as _}from"../log/index.js";import{resolveClientVersion as R}from"../util/client-version.js";import{UpgradeError as T,armWindowsUpgradeHandoff as N,armWindowsUpgradeRollback as Q,cleanupPendingBackup as O,collectEnvInfo as ee,discardPendingStaging as k,finalizePendingTransaction as P,getUpgradeLogTail as te,isVersionAvailable as ae,npmInstall as re,normalizeUpgradeVersion as ie,pendingExists as G,preflightCheck as ne,readPending as E,readPendingPackageVersion as se,removeWindowsHandoffFiles as M,rollbackPendingInstall as oe,updatePending as I,upgradeLog as n,verifyInstalledVersion as de,writePending as ce}from"./npm-upgrader.js";import{isRetriableRegistryError as le,isTransientInstallError as ue}from"../installer/npm-registry.js";const pe=360*60*1e3,he=300*1e3,W=1800*1e3,fe=2,ge=3,me=10,L="grix-connector",C=1e4,x=15,_e=2e3,we=8e3,ve=1e3,ye=2e3,be=120*1e3,q=1e4,Ae=6e4,Se=3,Te=600*1e3,Pe=new Set(["preparing","staged","handoff_ready","activating","verifying","healthy","rolling_back","rolled_back","finalizing","failed"]),V=600*1e3,D=1800*1e3;function $(s,e=V){const t=Date.parse(s.activation_started_at??s.upgraded_at??"");return Number.isNaN(t)?!0:Date.now()-t>e}function H(s){const e=Date.parse(s.verification_started_at??s.activation_started_at??"");return Number.isNaN(e)||Date.now()-e>be}function Re(s){if((s.guardian_handback_count??0)>=Se)return!1;const e=Date.parse(s.guardian_handback_at??"");return Number.isNaN(e)?!0:Date.now()-e>=Te}function ke(s){if(!Number.isInteger(s)||(s??0)<=0)return!1;try{return process.kill(s,0),!0}catch{return!1}}function A(){const s=g(_.base,"bin"),e=g(s,"upgrade-guardian.mjs");if(v(e))return e;const t=g(s,"upgrade-guardian.fallback.mjs");return v(t)?t:null}function B(s){if(!ke(s.guardian_pid))return!1;try{const t=JSON.parse(S(g(_.data,"upgrade-monitor-heartbeat.json"),"utf-8"));if(t.transaction_id===s.transaction_id&&t.pid===s.guardian_pid&&typeof t.updated_at=="number"&&Date.now()-t.updated_at<=q)return!0}catch{}const e=Date.parse(s.guardian_started_at??"");return Number.isFinite(e)&&Date.now()-e<=q}function F(s){return new URL(s.replace(/^wss:/,"https:").replace(/^ws:/,"http:")).origin}function j(){return g(_.data,"upgrade-state.json")}function z(){const s=j();if(!v(s))return{daily_attempts:{},version_attempts:{}};try{return JSON.parse(S(s,"utf-8"))}catch{return{daily_attempts:{},version_attempts:{}}}}function Ee(s){const e=j();X(Z(e),{recursive:!0});const t=e+".tmp";Y(t,JSON.stringify(s),"utf-8"),J(t,e)}function K(){return new Date().toISOString().slice(0,10)}class Ce{agentConfigs;isBusy;closeAdmission;options;timer=null;initialTimer=null;pendingTimer=null;guardianWaitLoggedAt=0;guardianWaitLoggedPhase="";recordedRollbackFailures=new Set;posixGuardianResumeAttempted=new Set;running=!1;stopped=!1;constructor(e,t,i,a={}){this.agentConfigs=e,this.isBusy=t,this.closeAdmission=i,this.options=a}async start(){await this.handlePendingOnStartup(),this.initialTimer=setTimeout(()=>{this.stopped||(this.runCheck(),!this.stopped&&(this.timer=setInterval(()=>this.runCheck(),pe)))},he)}stop(){this.stopped=!0,this.initialTimer&&(clearTimeout(this.initialTimer),this.initialTimer=null),this.timer&&(clearInterval(this.timer),this.timer=null),this.pendingTimer&&(clearTimeout(this.pendingTimer),this.pendingTimer=null)}triggerCheck(){this.stopped||this.runCheck()}async checkForUpdate(){return this.agentConfigs.length===0?{available:!1}:(await Promise.all(this.agentConfigs.map(i=>this.queryUpgrade(i)))).find(i=>i.available&&i.release)??{available:!1}}async handlePendingOnStartup(){if(!G())return;let e=E();if(!e){n("daemon startup: pending journal is unreadable; preserving it and blocking automatic upgrade for manual recovery");return}if(e.phase&&!Pe.has(e.phase)&&(e=I({phase:"failed",error_code:"INVALID_UPGRADE_STATE",error_msg:`Unknown upgrade transaction phase: ${String(e.phase)}`},e.transaction_id)),e.phase==="finalizing"){P(e)||(n(`terminal transaction ${e.transaction_id??"legacy"} is waiting for its shared lock to be released`),this.schedulePendingReconcile());return}if(e.phase==="preparing"||e.phase==="staged"){const o=!!(e.wrapper_pause_file&&v(e.wrapper_pause_file));if(e.phase==="staged"&&o){this.schedulePendingReconcile();return}k(e),e=I({phase:"rolled_back",error_code:"UPGRADE_INTERRUPTED",error_msg:"Upgrade stopped before Windows activation started"})}if(e.phase==="verifying"){if(B(e)&&!H(e)){this.schedulePendingReconcile();return}if(H(e)&&e.transaction_id&&["win32","darwin"].includes(this.options.platform??process.platform)&&this.options.requestGracefulShutdown){const o=`Target ${e.target_version} did not become healthy before the Windows verification deadline`;Q(e.transaction_id,o),n(`${o}; requesting graceful shutdown for offline rollback`),await this.options.requestGracefulShutdown("upgrade");return}await this.resumeWindowsGuardian(),this.schedulePendingReconcile();return}const t=new Set(["handoff_ready","activating","rolling_back"]);if(e.phase&&t.has(e.phase)){if(B(e)){this.logGuardianWait(e),this.schedulePendingReconcile();return}if($(e)&&e.transaction_id&&(this.options.platform??process.platform)==="win32"&&this.options.requestGracefulShutdown&&Re(e)){const o=`Guardian did not settle ${e.phase} for ${e.target_version} before the Windows activation deadline`;I({guardian_handback_count:(e.guardian_handback_count??0)+1,guardian_handback_at:new Date().toISOString()},e.transaction_id),n(`${o}; requesting graceful shutdown so the wrapper re-runs the guardian`),await this.options.requestGracefulShutdown("upgrade");return}if($(e))n(`daemon startup: guardian did not settle ${e.phase} for ${e.target_version} and cannot be resumed; collapsing the transaction by version verdict`),M(),k(e);else{await this.resumeWindowsGuardian(),this.logGuardianWait(e),this.schedulePendingReconcile();return}}if(e.phase==="failed"){n(`daemon startup: upgrade transaction requires manual recovery: ${e.error_msg??e.error_code??"unknown error"}`);return}const i=se(e),a=R(),r=e.phase==="healthy",d=e.phase==="rolled_back",h=!i||i===e.target_version,c=r?a===e.target_version&&i===e.target_version:!d&&a===e.target_version&&h;if(!c&&h&&!d){if(!$(e)){if((this.options.platform??process.platform)!=="win32"&&i===e.target_version){const o=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.posixGuardianResumeAttempted.has(o)||(this.posixGuardianResumeAttempted.add(o),n(`resuming POSIX guardian for stalled activation of ${e.target_version}`),this.startGuardian())}this.logThrottled("posix-activation-wait",`daemon startup: runtime ${a} is waiting for guardian activation of ${e.target_version} (active package=${i??"unknown"})`),this.schedulePendingReconcile();return}n(`daemon startup: activation wait for ${e.target_version} expired (> ${V/6e4}min); no guardian will finish the switch, reporting by version verdict`)}const u=c?{from_version:e.from_version,to_version:e.target_version,status:"success"}:{from_version:e.from_version,to_version:e.target_version,status:"rolled_back",error_code:e.error_code??"STARTUP_CRASH",error_msg:e.error_msg,crash_count:e.crash_count};if(n(c?`daemon startup: version matches target ${e.target_version}, upgrade succeeded`:`daemon startup: runtime=${a}, active package=${i??"unknown"}, target=${e.target_version}; rolled back`),!c){const o=e.transaction_id??`${e.target_version}@${e.upgraded_at}`;this.recordedRollbackFailures.has(o)||(this.recordedRollbackFailures.add(o),this.recordFailure(e.target_version))}if(c){const o=await this.confirmHealthz(we,r?e.target_version:void 0,r?e.old_pid:void 0,r?e.activation_started_at:void 0);if(r&&!o){n("guardian marked healthy but target-version healthz is not yet observable; retrying reconciliation"),this.schedulePendingReconcile();return}n(o?"healthz confirmed healthy, reporting success":"healthz not confirmed within window; reporting success on version-match (guardian still guards crashes)")}if(!O(e)){n(`terminal transaction ${e.transaction_id??"legacy"} is waiting for antivirus-held artifact cleanup`),this.schedulePendingReconcile();return}if(await this.reportUpgrade(u)){P(e)||(n(`terminal transaction ${e.transaction_id??"legacy"} could not release its shared lock; retrying reconciliation`),this.schedulePendingReconcile());return}this.retryStartupReport(u,e,!1)}schedulePendingReconcile(){this.stopped||this.pendingTimer||(this.pendingTimer=setTimeout(()=>{this.pendingTimer=null,!this.stopped&&this.handlePendingOnStartup().catch(e=>{f.warn("upgrade",`Pending reconciliation failed: ${e instanceof Error?e.message:e}`),this.schedulePendingReconcile()})},ye))}logThrottled(e,t){const i=Date.now();e===this.guardianWaitLoggedPhase&&i-this.guardianWaitLoggedAt<Ae||(this.guardianWaitLoggedPhase=e,this.guardianWaitLoggedAt=i,n(t))}logGuardianWait(e){const t=String(e.phase??"");this.logThrottled(`guardian:${t}`,`transaction ${e.transaction_id??"legacy"} is ${t}; waiting for guardian verdict`)}async resumeWindowsGuardian(){if(this.options.resumeWindowsGuardian){await this.options.resumeWindowsGuardian();return}const e=A();if(!e)return;U(process.execPath,[e,"activate","--root",_.base],{detached:!0,stdio:"ignore",windowsHide:!0}).unref()}async retryStartupReport(e,t,i){const a=t?.transaction_id,r=()=>{const d=E();if(!(a&&d?.transaction_id!==a)){if(i&&!O(t)){n(`terminal backup cleanup is still blocked for ${a??"legacy transaction"}`),this.schedulePendingReconcile();return}P(t)||(n(`terminal shared lock release is still blocked for ${a??"legacy transaction"}`),this.schedulePendingReconcile())}};for(let d=2;d<=x;d++){if(this.stopped)return;const h=_e*(d-1)+Math.floor(Math.random()*1e3);if(await new Promise(c=>setTimeout(c,h)),this.stopped)return;if(await this.reportUpgrade(e)){r();return}}n(`startup report (${e.status}) not delivered after ${x} attempts; clearing pending to unblock future checks`),r()}async confirmHealthz(e,t,i,a){let r="19579";try{const c=g(_.data,"health-port");v(c)&&(r=S(c,"utf-8").trim()||r)}catch{}const d=`http://127.0.0.1:${r}/healthz`,h=Date.now()+e;for(;Date.now()<h&&!this.stopped;){try{const c=await fetch(d,{signal:AbortSignal.timeout(2e3)});if(c.ok){if(!t)return!0;const u=await c.json();if(u.status==="ok"&&u.version===t&&typeof u.pid=="number"&&u.pid>0&&u.pid!==i)try{const o=JSON.parse(S(g(_.base,"daemon-status.json"),"utf-8")),y=Date.parse(a??"");return!!(o.state==="running"&&o.pid===u.pid&&typeof o.updated_at=="number"&&Number.isFinite(y)&&o.updated_at>=y)}catch{return!1}}}catch{}await new Promise(c=>setTimeout(c,ve))}return!1}async runCheck(){if(!this.running){this.running=!0;try{await this.check()}catch(e){f.error("upgrade",`Check failed: ${e instanceof Error?e.message:e}`)}finally{this.running=!1}}}async check(){if(G()){await this.handlePendingOnStartup();return}if(this.agentConfigs.length===0)return;const i=(await Promise.all(this.agentConfigs.map(l=>this.queryUpgrade(l)))).find(l=>l.available&&l.release)?.release;if(!i)return;const a=R();let r;try{r=ie(i.version)}catch(l){const p=l;await this.reportUpgrade({from_version:a,to_version:String(i.version),status:"failed",error_code:p.code??"INVALID_VERSION",error_msg:p.message});return}const d=i.force,h=this.options.platform??process.platform,c=this.options.env??process.env;if(h==="win32"){const l=A(),p=c.GRIX_WRAPPER_SUPERVISED==="1";if(!p||!l||!this.options.requestGracefulShutdown){const m=`Windows automatic upgrade requires a refreshed service installation (${[...p?[]:["managed wrapper"],...l?[]:["external guardian"],...this.options.requestGracefulShutdown?[]:["graceful shutdown hook"]].join(", ")})`;n(m),await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:"WINDOWS_UPGRADE_UNSUPPORTED",error_msg:m});return}}const u=h==="darwin"&&!!A()&&!!this.options.requestGracefulShutdown;if(h==="darwin"&&!u&&n("macOS transactional upgrade prerequisites missing; falling back to the legacy guardian flow"),!d&&!this.checkRateLimit(r))return;const o=ne();if(!o.ok){await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:o.errorCode,error_msg:o.errorMsg});return}if(!await ae(L,r)){n(`target ${r} not resolvable on any registry yet; skipping this cycle (no failure recorded)`);return}n(`upgrade start: ${a} -> ${r}`);const y=Date.now();let w;try{w=ce(a,r,h==="win32"||u?"preparing":void 0).transaction_id,(await re(L,r,void 0,u?{activationMode:"staged"}:{}))?.activationRequired||de(r),n("npm install verified, reporting installed"),await this.reportUpgrade({from_version:a,to_version:r,status:"installed",duration_ms:Date.now()-y}),n("shutting down for restart");const p=await this.waitUntilIdle();if(p!=="idle")throw n(`upgrade aborted before lifecycle handoff: ${p}`),this.options.reopenLifecycleAdmission?.(),new T("UPGRADE_ABORTED",`Upgrade was aborted before lifecycle handoff (${p})`);if(h==="win32"){if(!A())throw new T("WINDOWS_UPGRADE_UNSUPPORTED","Windows external guardian disappeared before lifecycle handoff");N(process.pid,w),n("Windows handoff armed; requesting graceful daemon shutdown"),await this.options.requestGracefulShutdown("upgrade")}else if(u){if(!A())throw new T("MACOS_UPGRADE_UNSUPPORTED","macOS guardian helper disappeared before lifecycle handoff");N(process.pid,w),n("macOS handoff armed; spawning guardian before graceful daemon shutdown"),await this.resumeWindowsGuardian(),await this.options.requestGracefulShutdown("upgrade")}else this.startGuardian(),process.kill(process.pid,"SIGTERM")}catch(l){const p=l instanceof T?l.code:"NPM_INSTALL_FAILED",b=l instanceof Error?l.message:String(l);n(`upgrade failed: ${p} ${b}`);const m=E();if(w&&m?.transaction_id===w&&(h==="win32"||u?(M(),k(m)):oe(m)&&n("upgrade failed before restart; previous package restored atomically"),P(m)||(n(`failed transaction ${w} is waiting for shared lock release`),this.schedulePendingReconcile()),this.options.reopenLifecycleAdmission?.()),p==="UPGRADE_IN_PROGRESS")return;le(b)||ue(b)||p==="NPM_TIMEOUT"?n(`failure is retriable (${p}); skipping cooldown, will retry next cycle`):this.recordFailure(r),await this.reportUpgrade({from_version:a,to_version:r,status:"failed",error_code:p,error_msg:b,duration_ms:Date.now()-y,upgrade_log:te()})}}async waitUntilIdle(){if(this.isBusy?.()){n(`active tasks detected, waiting up to ${Math.round(D/6e4)}min until idle before restart (admission stays open while waiting)`);const e=Date.now()+D,t=5e3,i=600*1e3;let a=Date.now();for(;this.isBusy()&&!this.stopped;){if(Date.now()>=e)return f.warn("upgrade",`Idle wait exceeded ${Math.round(D/6e4)}min; abandoning this upgrade attempt. Admission was never closed, so agents kept serving. A busy state that never clears usually means an adapter leaked its busy flag \u2014 check for a turn that never completed.`),n("idle wait timed out; upgrade attempt abandoned without ever closing admission"),"timeout";await new Promise(r=>setTimeout(r,t)),Date.now()-a>=i&&(a=Date.now(),n("still waiting for active tasks to finish before restart"))}}return this.stopped?"stopped":(this.closeAdmission?.(),this.isBusy?.()?(n("work arrived while closing admission; reopening and abandoning this upgrade attempt"),this.options.reopenLifecycleAdmission?.(),"busy-again"):(n("all tasks completed, proceeding with restart"),"idle"))}checkRateLimit(e){const t=z();if(t.last_failure_at){const d=Date.now()-new Date(t.last_failure_at).getTime();if(d<W)return f.info("upgrade",`In cooldown, ${(W-d)/6e4}m remaining`),!1}const i=t.version_attempts[e]??0;if(i>=ge)return f.info("upgrade",`Version ${e} already tried ${i} times, skipping`),!1;const a=K(),r=t.daily_attempts[a]??0;return r>=fe?(f.info("upgrade",`Already ${r} attempts today, skipping`),!1):!0}recordFailure(e){const t=z(),i=K();t.last_failure_at=new Date().toISOString(),t.last_failure_version=e,t.daily_attempts[i]=(t.daily_attempts[i]??0)+1,t.version_attempts[e]=(t.version_attempts[e]??0)+1;for(const r of Object.keys(t.daily_attempts))r<i&&delete t.daily_attempts[r];const a=Object.keys(t.version_attempts);for(const r of a.slice(0,Math.max(0,a.length-me)))r!==e&&delete t.version_attempts[r];Ee(t)}async queryUpgrade(e){const i=`${F(e.wsUrl)}/v1/agent-api/upgrade/check?`+new URLSearchParams({client_type:"grix-connector",client_version:R(),channel:e.channel??"stable",platform:process.platform,arch:process.arch}).toString();try{const a=await fetch(i,{headers:{Authorization:`Bearer ${e.apiKey}`},signal:AbortSignal.timeout(C)});if(!a.ok)return f.warn("upgrade",`Check API returned ${a.status}`),{available:!1};const r=await a.json();return r.code!==0?{available:!1}:r.data}catch(a){return f.warn("upgrade",`Check API error: ${a instanceof Error?a.message:a}`),{available:!1}}}async reportUpgrade(e){if(this.agentConfigs.length===0)return!1;const t={client_type:"grix-connector",...e.npm_version?e:{...e,...ee()}};return(await Promise.all(this.agentConfigs.map(async a=>{const d=`${F(a.wsUrl)}/v1/agent-api/upgrade/report`;try{return(await fetch(d,{method:"POST",headers:{Authorization:`Bearer ${a.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:AbortSignal.timeout(C)})).ok}catch{return!1}}))).some(a=>a)}startGuardian(){const e=g(_.base,"bin","upgrade-guardian.sh");if(process.platform==="win32"||!v(e)){f.info("upgrade","Guardian not available on this platform, skipping");return}try{const t=U(e,[],{detached:!0,stdio:"ignore"});t.unref(),n(`guardian started (pid ${t.pid})`)}catch(t){f.warn("upgrade",`Failed to start guardian: ${t instanceof Error?t.message:t}`)}}}export{Ce as UpgradeChecker};
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: grix-admin
3
- description: Grix agent administration through the `grix_admin` tool — create remote API agents, manage agent categories, rotate API keys — and bind an agent's remote parameters into the local grix-connector config with reload and verification. Trigger when the user asks to create a new agent, bind an agent to this connector, or manage agent categories.
4
- trigger: When the user asks to create a new Grix agent, bind an existing agent's parameters into the local grix-connector, or list / create / update / assign agent categories
3
+ description: Grix agent administration through the `grix_admin` tool — create remote API agents, manage agent categories, rotate API keys — and bind an agent's remote parameters into the local grix-connector config with reload and verification. For "create me an agent for X" requests, run `grix-egg` discovery first; use this skill when the user already has agent parameters to bind, asks for category management, or grix-egg found no matching egg / the user wants a blank agent.
4
+ trigger: When the user wants to bind an existing agent's parameters into the local grix-connector, list / create / update / assign agent categories, or create a blank agent after grix-egg discovery found nothing suitable
5
5
  ---
6
6
 
7
7
  # Grix Agent Admin
@@ -16,6 +16,11 @@ otherwise pick the one that matches the ask.
16
16
  | [`create-and-connector-bind`](#workflow-b--create-and-connector-bind) | Create a remote agent via `grix_admin`, then run `connector-bind-local` |
17
17
  | [`category-manage`](#workflow-c--category-manage) | List / create / update / assign categories via `grix_admin` |
18
18
 
19
+ **Creating a new agent from a business description?** Run the `grix-egg` skill
20
+ first (search the egg market, let the owner hatch a ready-made egg). Come here
21
+ for creation only when no egg matched or the user explicitly wants a blank
22
+ agent — and say so in your reply.
23
+
19
24
  All remote operations go through the `grix_admin` tool on the current
20
25
  account's WS channel — never hand-written HTTP or scripts, never across
21
26
  accounts, and never ask the user for a website account or password.
@@ -1,39 +1,93 @@
1
1
  ---
2
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: 'When you receive an install instruction containing a "技能包: <URL>" line asking you to install that skill package into your own skill directory'
3
+ description: "Grix egg market — the FIRST entry when the user wants a new agent or assistant for some purpose: search published eggs (skill and persona packages) by the business description with `grix_egg_search` / `grix_egg_get`, present matches, then install the chosen egg yourself from the download URL `grix_egg_get` returns (skill package into your own skill directory, or persona package into a new agent via grix-admin); only when nothing fits do you fall back to grix-admin to create a bare agent. Also handles the platform install instruction (a line like \"技能包: <URL>\")."
4
+ trigger: 'When the user wants to create a new agent / assistant for a role or business need, asks what ready-made eggs (skills, personas) exist, or when you receive an install instruction containing a "技能包: <URL>" line'
5
5
  ---
6
6
 
7
- # Grix Egg — Skill Package Self-Install
7
+ # Grix Egg
8
8
 
9
- For an agent like you, incubating an egg means exactly one thing: **installing a
10
- skill package into your own skill directory** so you can use it from then on.
9
+ Two jobs, in the order they happen:
11
10
 
12
- When you receive an install instruction containing a `技能包: <URL>` line, use
13
- this skill to install it, self-check, and send back an install-status card. The
14
- whole job is done by you on this machine no other agent installs it for you,
15
- and the main agent cannot touch your skill directory.
11
+ 1. [Discovery](#part-1--discovery-find-an-egg-before-creating-an-agent) the
12
+ user wants a new agent: find a ready-made egg first.
13
+ 2. [Self-install](#part-2--self-install-技能包-url)install a skill package
14
+ into your own skill directory, from a URL you got via `grix_egg_get` or
15
+ from the platform's `技能包: <URL>` instruction.
16
16
 
17
- ## What the instruction gives you
17
+ ## Part 1 Discovery: find an egg before creating an agent
18
18
 
19
- - `技能包: <URL>` download URL of the skill package to install (always present).
20
- - `install_id: <ID>` the ID of this install; echo it verbatim in the status card (always present).
21
- - `grix agent id: <ID>` your own grix agent id; use it as `target_agent_id` in the status card (may be present).
19
+ When the user describes an agent they want ("帮我建一个跟进外贸询盘的助手",
20
+ "I need a bot that reviews PRs"), **do not** go to `grix-admin` yet. Search the
21
+ egg market first; a published egg gives them a tested skill set or persona in
22
+ one tap.
22
23
 
23
- ## What you must do
24
+ ### Search strategy
24
25
 
25
- 1. Extract the download URL after `技能包:`.
26
- 2. Determine your own skill directory: it is **the parent of the directory this grix-egg skill lives in**. E.g. if this skill is at `~/.codex/skills/grix-egg/`, the skill directory is `~/.codex/skills/`; at `~/.claude/skills/grix-egg/` it is `~/.claude/skills/`, and so on.
27
- 3. Download and unpack the skill package.
28
- 4. Copy every unpacked skill (any directory containing a `SKILL.md`) into your skill directory, whole directory at a time.
29
- 5. Confirm the new skill's `SKILL.md` now exists in your skill directory.
30
- 6. **Send back the install-status card** (see below) so the platform can close out this install as success/failure.
31
- 7. Then tell the user, in one natural-language sentence, which skill(s) were installed.
26
+ The backend matches keywords term-by-term (AND) against name + description +
27
+ category, so a whole sentence returns nothing. Instead:
32
28
 
33
- ## Steps 2–5: install (run in shell)
29
+ 1. Distill the description into **2–4 short keywords**: the role noun, the
30
+ domain, the core action (e.g. `外贸`, `询盘`, `报价`; `code review`, `PR`).
31
+ 2. Call `grix_egg_search` **once per keyword** (`pageSize` 10), in the user's
32
+ language and `locale`. Merge the results, rank by number of keyword hits
33
+ then `install_count`.
34
+ 3. If keywords return nothing, browse by category: search with no keyword
35
+ and a likely `categoryId` (categories come back in `category_id` /
36
+ `category_name` of any result, or run a broad search first).
37
+ 4. Optionally `grix_egg_get` the top candidates for the full description and
38
+ `version_desc`.
34
39
 
35
- Replace the two angle-bracket placeholders with real values, then run.
36
- `SKILLS_DIR` is the parent of the directory this skill lives in.
40
+ ### Present candidates
41
+
42
+ Show at most 3–5 eggs, each with: name (+ emoji), one-line description,
43
+ category, `install_count`, and what it can do for the user:
44
+
45
+ - `can_create_agent: true` — hatches into a **new agent** (persona + skills).
46
+ - `existing_agent_client_types` non-empty — installs as a **skill into an
47
+ existing agent** of those client types.
48
+
49
+ Ask the user which one to hatch. Do not invent capabilities that are not in
50
+ the egg description.
51
+
52
+ ### Install the chosen egg
53
+
54
+ `grix_egg_get` returns the package URLs: `skill_zip_url` (+ `skill_zip_sha256`)
55
+ and `persona_zip_url` (+ `persona_zip_sha256`). No App round-trip is needed.
56
+
57
+ - **Skill into yourself** (`existing_agent_client_types` includes your client
58
+ type): download `skill_zip_url`, verify the sha256 when present, then run the
59
+ Part 2 script with that URL. There is no `install_id` in this path, so skip
60
+ the status card and just report the installed skill(s).
61
+ - **New agent** (`can_create_agent: true`): create and bind the agent with
62
+ `grix-admin` (`create-and-connector-bind`), then apply `persona_zip_url` to
63
+ it the same way the platform's `人格包: <URL>` instruction would.
64
+ - No egg fits, or the user explicitly wants a blank agent → hand over to
65
+ `grix-admin` (`create-and-connector-bind`) and say clearly that no
66
+ ready-made egg matched.
67
+
68
+ If the platform itself sends a `技能包: <URL>` instruction (the owner hatched
69
+ from the App), run Part 2 — that path carries an `install_id` and needs the
70
+ status card.
71
+
72
+ ## Part 2 — Self-install (`技能包: <URL>`)
73
+
74
+ Incubating an egg means exactly one thing for you: **installing a skill
75
+ package into your own skill directory**. The whole job runs on this machine;
76
+ no other agent installs it for you.
77
+
78
+ The instruction gives you:
79
+
80
+ - `技能包: <URL>` — download URL of the package (always present).
81
+ - `install_id: <ID>` — echo it verbatim in the status card (always present).
82
+ - `grix agent id: <ID>` — your own agent id; use as `target_agent_id` in the
83
+ status card (may be present).
84
+
85
+ Steps: (1) extract the URL; (2) your skill directory is **the parent of the
86
+ directory this skill lives in** (e.g. `~/.claude/skills/grix-egg/` →
87
+ `~/.claude/skills/`); (3) download, unpack, copy every directory containing a
88
+ `SKILL.md` into it, overwriting a same-name skill; (4) confirm the new
89
+ `SKILL.md` exists; (5) send the status card; (6) tell the user in one sentence
90
+ which skill(s) were installed.
37
91
 
38
92
  ```bash
39
93
  set -e
@@ -45,7 +99,6 @@ curl -fsSL "$URL" -o "$TMP/skill.zip"
45
99
  mkdir -p "$TMP/unpacked"
46
100
  unzip -o -q "$TMP/skill.zip" -d "$TMP/unpacked"
47
101
 
48
- # Copy every skill directory containing a SKILL.md into the skill directory
49
102
  found=0
50
103
  while IFS= read -r -d '' f; do
51
104
  d="$(dirname "$f")"
@@ -53,7 +106,7 @@ while IFS= read -r -d '' f; do
53
106
  found=1
54
107
  done < <(find "$TMP/unpacked" -name SKILL.md -print0)
55
108
 
56
- # Fallback: SKILL.md sits loose at the unpack root — create a skill directory named after its `name` and move everything in
109
+ # Fallback: SKILL.md sits loose at the unpack root
57
110
  if [ "$found" -eq 0 ] && [ -f "$TMP/unpacked/SKILL.md" ]; then
58
111
  name="$(grep -m1 '^name:' "$TMP/unpacked/SKILL.md" | sed 's/^name:[[:space:]]*//')"
59
112
  [ -z "$name" ] && name="egg-skill"
@@ -61,38 +114,29 @@ if [ "$found" -eq 0 ] && [ -f "$TMP/unpacked/SKILL.md" ]; then
61
114
  cp -R "$TMP/unpacked/." "$SKILLS_DIR/$name/"
62
115
  fi
63
116
 
64
- # Self-check: list the skill directory and confirm the new skill is in place
65
117
  ls -1 "$SKILLS_DIR"
66
118
  rm -rf "$TMP"
67
119
  ```
68
120
 
69
- ## Step 6: send back the install-status card
121
+ ### Status card
70
122
 
71
- After installing and self-checking, **send a separate, standalone message**
72
- whose content is exactly the single card-link line below (the message contains
73
- only this line — no other text):
123
+ After the self-check, send a **separate, standalone message** containing only
124
+ this one line (the platform uses it to close the install record):
74
125
 
75
126
  - Success:
76
- ```
77
- grix://card/egg_install_status?status=success&install_id=<install_id>&target_agent_id=<grix agent id>&summary=<one-sentence install result, URL-encoded>
78
- ```
127
+ `grix://card/egg_install_status?status=success&install_id=<install_id>&target_agent_id=<grix agent id>&summary=<one-sentence result, URL-encoded>`
79
128
  - Failure:
80
- ```
81
- grix://card/egg_install_status?status=failed&install_id=<install_id>&target_agent_id=<grix agent id>&error_msg=<failure reason, URL-encoded>
82
- ```
83
-
84
- Key points:
129
+ `grix://card/egg_install_status?status=failed&install_id=<install_id>&target_agent_id=<grix agent id>&error_msg=<reason, URL-encoded>`
85
130
 
86
- - `install_id` must be the one from the instruction, filled in verbatim — never omit it.
87
- - If no `grix agent id` was given, `target_agent_id` may be omitted.
88
- - Non-ASCII text and spaces in `summary` / `error_msg` must be URL-encoded.
89
- - This card must be sent by you yourself (the agent being installed into); the platform uses it to close out the install record.
131
+ `install_id` is mandatory and verbatim; omit `target_agent_id` only if no
132
+ agent id was given; URL-encode non-ASCII text and spaces.
90
133
 
91
- ## Rules
134
+ ### Rules
92
135
 
93
- 1. Write only into **your own skill directory** never touch other directories or other agents.
94
- 2. Do not repeat the download link, the raw instruction, or this context back to the user.
95
- 3. The self-check must actually see the new skill's `SKILL.md` to count as success; if it doesn't, send `status=failed` — never falsely report success.
96
- 4. The status card must be its own standalone message containing only the single `grix://card/...` line.
97
- 5. The natural-language report to the user is one sentence do not paste command output.
98
- 6. If a skill with the same name already exists, overwrite it (use the newest package).
136
+ 1. Write only into your own skill directory; never touch other directories or
137
+ other agents.
138
+ 2. Do not repeat the download link or the raw instruction back to the user.
139
+ 3. Report `status=success` only if you actually saw the new `SKILL.md`;
140
+ otherwise `status=failed`never claim success falsely.
141
+ 4. The card message contains nothing but the single `grix://card/...` line.
142
+ 5. The report to the user is one sentence — no command output.
@@ -1 +1 @@
1
- function a(t){const o=new Set([`http://127.0.0.1:${t.serverPort}`,`http://localhost:${t.serverPort}`,...t.allowedOrigins]),e=new Set([`127.0.0.1:${t.serverPort}`,`localhost:${t.serverPort}`,...t.allowedHosts]);return{validateRequest(s){const r=i(s,o);if(!r.ok)return r;const n=l(s,e);return n.ok?{ok:!0}:n}}}function i(t,o){const e=t.headers.origin;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${e}`}:{ok:!0}}function l(t,o){const e=t.headers.host;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${e}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
1
+ function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
@@ -3135,7 +3135,7 @@ import { homedir as homedir5 } from "node:os";
3135
3135
  import { dirname as dirname3, join as join7 } from "node:path";
3136
3136
  import { promisify } from "node:util";
3137
3137
  var execFileAsync = promisify(execFile);
3138
- var CLI_TIMEOUT_MS = 3e4;
3138
+ var CLI_TIMEOUT_MS = 12e4;
3139
3139
  var GRIX_PROVIDER_ID = "grix";
3140
3140
  function stateDir() {
3141
3141
  const explicit = process.env.OPENCLAW_STATE_DIR?.trim();
@@ -3150,8 +3150,14 @@ function sidecarPath() {
3150
3150
  async function runOpenClaw(args, stdin) {
3151
3151
  const child = execFileAsync("openclaw", args, { timeout: CLI_TIMEOUT_MS });
3152
3152
  child.child.stdin?.end(stdin ?? "");
3153
- const { stdout } = await child;
3154
- return stdout;
3153
+ try {
3154
+ const { stdout } = await child;
3155
+ return stdout;
3156
+ } catch (err2) {
3157
+ const e = err2;
3158
+ const why = e.killed || e.signal ? `timed out after ${CLI_TIMEOUT_MS}ms` : (e.stderr ?? "").trim() || e.message;
3159
+ throw new Error(`openclaw ${args.join(" ")} failed: ${why}`);
3160
+ }
3155
3161
  }
3156
3162
  function configFilePath() {
3157
3163
  const explicit = process.env.OPENCLAW_CONFIG_PATH?.trim();
@@ -10866,6 +10872,51 @@ var TOOLS = [
10866
10872
  }
10867
10873
  }
10868
10874
  },
10875
+ {
10876
+ name: "grix_egg_search",
10877
+ description: "Search the Grix egg market (published skill/persona packages the owner can hatch into a new agent or install into an existing one). Keyword matching is per-term AND over name + description + category, so search short keywords (one concept per call) rather than a whole sentence; omit keyword to browse a category. Results include id, name, description, category, install_count, can_create_agent, and existing_agent_client_types.",
10878
+ inputSchema: {
10879
+ type: "object",
10880
+ properties: {
10881
+ keyword: { type: "string", description: "Short search keyword(s); terms are ANDed. Omit to list by category." },
10882
+ categoryId: { type: "string", description: "Optional egg category ID to restrict results." },
10883
+ locale: { type: "string", description: "Optional locale such as zh-CN or en; defaults to the owner's locale." },
10884
+ page: { type: "integer", description: "Page number, starting from 1." },
10885
+ pageSize: { type: "integer", description: "Items per page (1-50), default 20." }
10886
+ }
10887
+ },
10888
+ validation: {
10889
+ required: [],
10890
+ properties: {
10891
+ keyword: { type: "string", maxLength: 200 },
10892
+ categoryId: { type: "string" },
10893
+ locale: { type: "string", maxLength: 16 },
10894
+ page: { type: "integer", minimum: 1 },
10895
+ pageSize: { type: "integer", minimum: 1, maximum: 50 }
10896
+ }
10897
+ }
10898
+ },
10899
+ {
10900
+ name: "grix_egg_get",
10901
+ description: "Get one Grix egg by id \u2014 full description, current version, and whether it can create a new agent or install into existing agents. Use after grix_egg_search to show details before the owner decides to hatch.",
10902
+ inputSchema: {
10903
+ type: "object",
10904
+ properties: {
10905
+ id: { type: "string", description: "Egg ID from grix_egg_search." },
10906
+ locale: { type: "string", description: "Optional locale." },
10907
+ version: { type: "integer", description: "Optional specific version; defaults to the current one." }
10908
+ },
10909
+ required: ["id"]
10910
+ },
10911
+ validation: {
10912
+ required: ["id"],
10913
+ properties: {
10914
+ id: { type: "string" },
10915
+ locale: { type: "string", maxLength: 16 },
10916
+ version: { type: "integer", minimum: 1 }
10917
+ }
10918
+ }
10919
+ },
10869
10920
  {
10870
10921
  name: "grix_skill_set",
10871
10922
  description: "Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",
@@ -11098,6 +11149,10 @@ function toolCallToInvoke(toolName, args) {
11098
11149
  return buildAccessControlInvoke(args);
11099
11150
  case "grix_widget_visitor_ban":
11100
11151
  return buildWidgetVisitorBanInvoke(args);
11152
+ case "grix_egg_search":
11153
+ return buildEggSearchInvoke(args);
11154
+ case "grix_egg_get":
11155
+ return buildEggGetInvoke(args);
11101
11156
  case "grix_skill_set":
11102
11157
  return buildSkillSetInvoke(args);
11103
11158
  case "grix_skill_get":
@@ -11128,6 +11183,21 @@ function buildQueryInvoke(args) {
11128
11183
  if (args.beforeId != null) params.before_id = args.beforeId;
11129
11184
  return { action: invokeAction, params };
11130
11185
  }
11186
+ function buildEggSearchInvoke(args) {
11187
+ const params = {};
11188
+ if (args.keyword != null) params.keyword = args.keyword;
11189
+ if (args.categoryId != null) params.category_id = args.categoryId;
11190
+ if (args.locale != null) params.locale = args.locale;
11191
+ if (args.page != null) params.page = args.page;
11192
+ if (args.pageSize != null) params.page_size = args.pageSize;
11193
+ return { action: "egg_search", params };
11194
+ }
11195
+ function buildEggGetInvoke(args) {
11196
+ const params = { id: args.id };
11197
+ if (args.locale != null) params.locale = args.locale;
11198
+ if (args.version != null) params.version = args.version;
11199
+ return { action: "egg_get", params };
11200
+ }
11131
11201
  var AUDIT_DATA_ACTION_MAP = {
11132
11202
  get_manifest: "audit_get_manifest",
11133
11203
  list_spans: "audit_list_spans",
@@ -11618,6 +11688,49 @@ function createGrixQueryTool(api, ctx) {
11618
11688
  };
11619
11689
  }
11620
11690
 
11691
+ // src/openclaw/admin/egg-market-tools.ts
11692
+ function createEggMarketTool(toolName, label, api, ctx) {
11693
+ const def = TOOLS.find((t) => t.name === toolName);
11694
+ const contextAccountId = ctx?.agentAccountId;
11695
+ return {
11696
+ name: toolName,
11697
+ label,
11698
+ description: def.description,
11699
+ parameters: def.inputSchema,
11700
+ async execute(_toolCallId, params) {
11701
+ try {
11702
+ const accountId = resolveStrictToolAccountId({ toolName, contextAccountId });
11703
+ const account = resolveGrixAccount({
11704
+ cfg: api.config,
11705
+ accountId,
11706
+ strictAccountScope: true
11707
+ });
11708
+ if (!account.enabled) {
11709
+ throw new Error(`Grix account "${account.accountId}" is disabled.`);
11710
+ }
11711
+ if (!account.configured) {
11712
+ throw new Error(`Grix account "${account.accountId}" is not configured.`);
11713
+ }
11714
+ const { action, params: invokeParams } = toolCallToInvoke(toolName, params);
11715
+ const client = requireActiveAibotClient(accountId);
11716
+ const data = await client.agentInvoke(action, invokeParams);
11717
+ return jsonToolResult({ ok: true, accountId: account.accountId, action, data });
11718
+ } catch (err2) {
11719
+ return jsonToolResult({
11720
+ ok: false,
11721
+ error: err2 instanceof Error ? err2.message : String(err2)
11722
+ });
11723
+ }
11724
+ }
11725
+ };
11726
+ }
11727
+ function createGrixEggSearchTool(api, ctx) {
11728
+ return createEggMarketTool("grix_egg_search", "Grix Egg Search", api, ctx);
11729
+ }
11730
+ function createGrixEggGetTool(api, ctx) {
11731
+ return createEggMarketTool("grix_egg_get", "Grix Egg Get", api, ctx);
11732
+ }
11733
+
11621
11734
  // src/openclaw/admin/audit-data-tool.ts
11622
11735
  var auditDataDef = TOOLS.find((tool) => tool.name === "grix_audit_data");
11623
11736
  function createGrixAuditDataTool(api, ctx) {
@@ -12836,6 +12949,8 @@ async function bindAgent(agentName) {
12836
12949
  var DEFAULT_TOOLS_ALLOW = [
12837
12950
  "message",
12838
12951
  "grix_query",
12952
+ "grix_egg_search",
12953
+ "grix_egg_get",
12839
12954
  "grix_group",
12840
12955
  "grix_register",
12841
12956
  "grix_message_send",
@@ -14473,6 +14588,8 @@ var plugin = {
14473
14588
  api.registerTool((ctx) => createGrixGroupTool(api, ctx), { optional: true });
14474
14589
  api.registerTool((ctx) => createGrixAdminTool(api, ctx), { optional: true });
14475
14590
  api.registerTool((ctx) => createGrixEggTool(api, ctx), { optional: true });
14591
+ api.registerTool((ctx) => createGrixEggSearchTool(api, ctx), { optional: true });
14592
+ api.registerTool((ctx) => createGrixEggGetTool(api, ctx), { optional: true });
14476
14593
  api.registerTool((ctx) => createGrixRegisterTool(api, ctx), { optional: true });
14477
14594
  api.registerTool((ctx) => createGrixUpdateTool(api, ctx), { optional: true });
14478
14595
  api.registerTool((ctx) => createGrixMessageSendTool(api, ctx), { optional: true });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: grix-admin
3
- description: Grix agent administration for OpenClaw — create remote API agents, manage agent categories, and rotate API keys through `grix_admin`, and bind an agent's remote parameters into the local OpenClaw configuration with validation and routing verification. Trigger when the user asks to create a new agent, bind an agent into this OpenClaw host, or manage agent categories.
3
+ description: Grix agent administration for OpenClaw — create remote API agents, manage agent categories, and rotate API keys through `grix_admin`, and bind an agent's remote parameters into the local OpenClaw configuration with validation and routing verification. For "create me an agent for X" requests, run `grix-egg` discovery first; use this skill when the user already has agent parameters to bind, asks for category management, or grix-egg found no matching egg / the user wants a blank agent.
4
4
  ---
5
5
 
6
6
  # Grix Agent Admin (OpenClaw)
@@ -23,6 +23,11 @@ Three workflows:
23
23
  for every remote call *inside* a workflow. `task` and `action` cannot be
24
24
  combined in one call.
25
25
 
26
+ **Creating a new agent from a business description?** Run the `grix-egg` skill
27
+ first (search the egg market, let the owner hatch a ready-made egg). Come here
28
+ for creation only when no egg matched or the user explicitly wants a blank
29
+ agent — and say so in your reply.
30
+
26
31
  All remote operations go through `grix_admin` on the current account's WS
27
32
  channel — never hand-written HTTP or scripts, never across accounts, and
28
33
  never ask the user for a website account or password.
@@ -1,10 +1,36 @@
1
1
  ---
2
2
  name: grix-egg
3
- description: "Program-driven hatching: AI only organizes parameters and calls `grix_egg`; the program handles creation, installation, binding, acceptance, and status reporting. Use for creating or installing an Agent from an egg package, including collecting and professionally organizing its name and behavioral introduction before remote creation."
3
+ description: "Grix egg market for OpenClaw — the FIRST entry when the user wants a new agent or assistant for some purpose: search published eggs (skill and persona packages) with `grix_egg_search` / `grix_egg_get`, present matches, and hatch the chosen one yourself from the package URLs `grix_egg_get` returns (via the `grix_egg` tool); only when nothing fits fall back to grix-admin. Hatching itself is program-driven: AI organizes parameters and calls `grix_egg`; the program handles creation, installation, binding, acceptance, and status reporting."
4
4
  ---
5
5
 
6
6
  # Grix Egg
7
7
 
8
+ ## 0. Discovery first — find an egg before creating an agent
9
+
10
+ When the user describes an agent they want ("帮我建一个跟进外贸询盘的助手",
11
+ "I need a bot that reviews PRs") and there is no install card yet, search the
12
+ egg market before anything else; do **not** go to `grix-admin`.
13
+
14
+ 1. Distill the description into **2–4 short keywords** (role noun, domain,
15
+ core action). The backend ANDs terms against name + description +
16
+ category, so a whole sentence returns nothing.
17
+ 2. Call `grix_egg_search` **once per keyword** (`pageSize` 10) in the user's
18
+ language / `locale`; merge, rank by keyword hits then `install_count`. If
19
+ nothing matches, browse by `categoryId` with no keyword.
20
+ 3. Show at most 3–5 candidates: name (+ emoji), one-line description,
21
+ category, `install_count`, and whether it hatches a **new agent**
22
+ (`can_create_agent`) or installs as a **skill into an existing agent**
23
+ (`existing_agent_client_types`). `grix_egg_get` gives the full text.
24
+ 4. The user picks one → `grix_egg_get` returns the package URLs
25
+ (`persona_zip_url` / `skill_zip_url`, each with a `*_sha256`). Call
26
+ `grix_egg` with structured parameters (section 3, no install card):
27
+ `route: create_new` + `agentName` + `introduction` + `downloadUrl` =
28
+ `persona_zip_url` for a new agent, or `route: existing` + `downloadUrl`
29
+ for an existing one; pass the sha256 as `packageHash`. No `installId`
30
+ exists in this path, so no status card is sent.
31
+ 5. No egg fits, or the user explicitly wants a blank agent → hand over to
32
+ `grix-admin` and say clearly that no ready-made egg matched.
33
+
8
34
  `grix-egg` follows a "program-first, AI-assists" approach.
9
35
  AI no longer manually relays create/bind/accept.
10
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "4.2.4",
3
+ "version": "4.2.6",
4
4
  "description": "Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",