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.
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.2.6.tgz +0 -0
- package/dist/assets/dsh-bridge/manifest.json +6 -6
- package/dist/core/mcp/tools.js +1 -1
- package/dist/core/upgrade/upgrade-checker.js +1 -1
- package/dist/default-skills/grix-admin/SKILL.md +7 -2
- package/dist/default-skills/grix-egg/SKILL.md +94 -50
- package/dist/mcp/stream-http/security.js +1 -1
- package/openclaw-plugin/index.js +120 -3
- package/openclaw-plugin/skills/grix-admin/SKILL.md +6 -1
- package/openclaw-plugin/skills/grix-egg/SKILL.md +27 -1
- package/package.json +1 -1
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.2.4.tgz +0 -0
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bridgeVersion": "4.2.
|
|
4
|
-
"tarball": "grix-dsh-bridge-4.2.
|
|
5
|
-
"size":
|
|
6
|
-
"unpackedSize":
|
|
7
|
-
"shasum": "
|
|
8
|
-
"integrity": "sha512-
|
|
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
|
}
|
package/dist/core/mcp/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},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.
|
|
4
|
-
trigger: When the user
|
|
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:
|
|
4
|
-
trigger: 'When you receive an install instruction containing a "技能包: <URL>" line
|
|
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
|
|
7
|
+
# Grix Egg
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
##
|
|
17
|
+
## Part 1 — Discovery: find an egg before creating an agent
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
+
### Search strategy
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
121
|
+
### Status card
|
|
70
122
|
|
|
71
|
-
After
|
|
72
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
134
|
+
### Rules
|
|
92
135
|
|
|
93
|
-
1. Write only into
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
1
|
+
function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
|
package/openclaw-plugin/index.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
3154
|
-
|
|
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.
|
|
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: "
|
|
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.
|
|
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",
|
|
Binary file
|