grix-connector 4.11.13 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/adapter/acp/acp-adapter.js +12 -12
- package/dist/adapter/claude/claude-bridge-server.js +1 -1
- package/dist/adapter/claude/claude-tools.js +1 -1
- package/dist/adapter/claude/claude-worker-client.js +1 -1
- package/dist/adapter/claude/mcp-http-launcher.js +2 -2
- package/dist/adapter/claude/result-timeout.js +1 -1
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.12.0.tgz +0 -0
- package/dist/assets/dsh-bridge/manifest.json +6 -6
- package/dist/bridge/acp-custom-toolbar.js +1 -0
- package/dist/bridge/bridge.js +2 -2
- package/dist/bridge/cwd-policy.js +1 -0
- package/dist/bridge/session-control-open.js +1 -1
- package/dist/bridge/text-command-session-control.js +1 -1
- package/dist/core/access/allowlist-store.js +1 -1
- package/dist/core/file-ops/list-files.js +1 -1
- package/dist/core/mcp/tools.js +1 -1
- package/dist/core/observability/sentry-worker.js +1 -0
- package/dist/core/observability/sentry.js +2 -1
- package/dist/grix.js +8 -8
- package/dist/log.js +2 -2
- package/dist/manager.js +1 -1
- package/dist/mcp/stream-http/config.js +1 -1
- package/dist/mcp/stream-http/connection-binding.js +1 -1
- package/dist/mcp/stream-http/tool-executor.js +1 -1
- package/dist/mcp/stream-http/tool-registry.js +1 -1
- package/dist/mcp/stream-http/tool-schemas.js +1 -1
- package/dist/service/service-manager.js +2 -2
- package/openclaw-plugin/index.js +57 -1
- package/package.json +1 -1
- package/dist/assets/dsh-bridge/grix-dsh-bridge-4.11.13.tgz +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import _ from"node:path";import{stat as v}from"node:fs/promises";import{SESSION_CONTROL_ERROR_CODES as u,SESSION_CONTROL_VERBS as
|
|
1
|
+
import _ from"node:path";import{stat as v}from"node:fs/promises";import{SESSION_CONTROL_ERROR_CODES as u,SESSION_CONTROL_VERBS as s}from"../adapter/claude/protocol-contract.js";import{AcpAdapter as g}from"../adapter/acp/index.js";import{log as l}from"../core/log/index.js";import{CWD_LOCKED_REJECT_MSG as a,isCwdLockedClientType as C}from"./cwd-policy.js";import{handleSessionControlCommand as c}from"./session-controller.js";const D=["deferredCallbacks","handleCodeWhaleSessionControlOpen","handleCodexSessionControlOpen","handleListSessionsTextCommand","handleOpenHumanSessionControlOpen","handlePiSessionControlOpen","handlePiSessionControlRestart","handleSessionControlCommand","handleSessionControlForPool","handleUnbindTextCommand","refreshAndPushAgyQuota","resolveAuditProvider","sendAgyBindingCard","sendAuditConfigurationError","sessionControlCtx","sessionControlSenders"],H=["aibotHandle","pool","bindingStore","sessionBindings","sessionProviderHints","sessionProviderQuotas","sessionProviderMeta","deferredMgr","auditController"];async function M(i,n,e,r,S,f){if(C(f)&&(n.verb===s.open||n.verb===s.unbind))return i.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),i.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:u.verbInvalid,msg:a,updated_at:Date.now()}),!0;if(S.state!=="absent"||n.verb===s.open)try{i.auditController.resolveSession({sessionId:e.session_id,provider:i.resolveAuditProvider(),extra:e.extra})}catch(t){return i.sendAuditConfigurationError(e,t),!0}if(n.verb===s.exec)return await i.handleSessionControlCommand(n,e),!0;if(n.verb===s.listSessions)return await i.handleListSessionsTextCommand(e),!0;if(n.verb===s.unbind)return await i.handleUnbindTextCommand(e),!0;if(r==="claude")return await i.handleSessionControlCommand(n,e),!0;if(r==="codex"&&n.verb===s.open)return await i.handleCodexSessionControlOpen(n,e),!0;if(r==="pi"&&n.verb===s.open)return await i.handlePiSessionControlOpen(n,e),!0;if(r==="pi"&&n.verb===s.restart)return await i.handlePiSessionControlRestart(e),!0;if((r==="openhuman"||r==="opencode")&&n.verb===s.open)return await i.handleOpenHumanSessionControlOpen(n,e),!0;if(r==="codewhale"&&n.verb===s.open)return await i.handleCodeWhaleSessionControlOpen(n,e),!0;if(i.aibotHandle.sendEventAck({event_id:e.event_id,session_id:e.session_id,received_at:Date.now()}),n.verb===s.open){const t=n.args.trim();if(!t)return i.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:u.cwdRequired,msg:"cwd is required",updated_at:Date.now()}),!0;try{const d=_.resolve(t);if(!(await v(d)).isDirectory())return i.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:u.invalidCwd,msg:`Path is not a directory: ${d}`,updated_at:Date.now()}),!0}catch(d){const o=String(d?.code??""),b=o==="ENOENT"?`Directory does not exist: ${_.resolve(t)}`:o==="EACCES"||o==="EPERM"?"Directory is not accessible":`Invalid path: ${t}`;return i.aibotHandle.sendEventResult({event_id:e.event_id,status:"failed",code:u.invalidCwd,msg:b,updated_at:Date.now()}),!0}}if(n.verb===s.open){const t=i.bindingStore.get(e.session_id);if(t?.cwd)try{await v(t.cwd)}catch{l.info("bridge",`Stale binding detected for session ${e.session_id}: ${t.cwd} no longer exists, clearing`),i.bindingStore.delete(e.session_id),i.sessionBindings.delete(e.session_id),i.sessionProviderHints.delete(e.session_id),i.sessionProviderQuotas.delete(e.session_id),i.sessionProviderMeta.delete(e.session_id);const d=i.pool.getSlot(e.session_id);d?.adapter instanceof g&&d.adapter.getSessionBindings().delete(e.session_id)}}if(await i.handleSessionControlForPool(n,e),r==="acp"&&n.verb===s.stop){const d=i.bindingStore.get(e.session_id)?.cwd??"";return await i.pool.removeSlot(e.session_id).catch(()=>{}),i.sessionBindings.delete(e.session_id),i.sessionProviderHints.delete(e.session_id),i.sessionProviderQuotas.delete(e.session_id),i.sessionProviderMeta.delete(e.session_id),i.aibotHandle.sendEventResult({event_id:e.event_id,status:"responded",msg:`Session worker stopped for ${d}`,updated_at:Date.now()}),!0}if(c(n,e,i.sessionControlCtx(e.session_id),{...i.sessionControlSenders(e.session_id),sendEventAck:()=>{}}),n.verb===s.open&&(await i.deferredMgr.release(e.session_id,i.deferredCallbacks()),r==="agy")){const t=i.bindingStore.get(e.session_id)?.cwd??"";t&&(i.sendAgyBindingCard(e.session_id,t),i.refreshAndPushAgyQuota(e.session_id,!0))}return!0}export{H as TEXT_COMMAND_SESSION_CONTROL_HOST_FIELDS,D as TEXT_COMMAND_SESSION_CONTROL_HOST_METHODS,M as dispatchSessionControlTextCommand};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readJSONFile as o,writeJSONFileAtomic as
|
|
1
|
+
import{readJSONFile as o,writeJSONFileAtomic as i}from"../util/json-file.js";function n(t){const r=o(t);return r&&typeof r=="object"&&"owners"in r&&Array.isArray(r.owners)?r.owners.filter(e=>typeof e=="string"&&e.trim().length>0):[]}async function s(t,r){await i(t,{owners:r})}export{n as readAllowlist,s as writeAllowlist};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const
|
|
1
|
+
import{readdir as r,stat as m}from"node:fs/promises";import{join as l,extname as d}from"node:path";const x={pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",txt:"text/plain",md:"text/markdown",csv:"text/csv",json:"application/json",xml:"application/xml",yaml:"text/yaml",yml:"text/yaml",html:"text/html",css:"text/css",js:"text/javascript",ts:"text/typescript",zip:"application/zip",rar:"application/x-rar-compressed","7z":"application/x-7z-compressed",tar:"application/x-tar",gz:"application/gzip",jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",mp4:"video/mp4",mov:"video/quicktime",avi:"video/x-msvideo",mkv:"video/x-matroska",webm:"video/webm",mp3:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",aac:"audio/aac"};function n(a){const p=d(a).slice(1).toLowerCase();return x[p]}async function f(a,p=!1){const c=await r(a,{withFileTypes:!0}),s=[];for(const i of c){if(!p&&i.name.startsWith("."))continue;const t=l(a,i.name),e={id:t,name:i.name,is_directory:i.isDirectory()};try{if(i.isDirectory()){const o=await m(t);e.modified_at=o.mtime.toISOString()}else{const o=await m(t);e.size=o.size,e.modified_at=o.mtime.toISOString(),e.mime_type=n(i.name)}}catch{}s.push(e)}return s.sort((i,t)=>i.is_directory!==t.is_directory?i.is_directory?-1:1:i.name.localeCompare(t.name)),s}export{f as listFiles,n as resolveMimeType};
|
package/dist/core/mcp/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$",description:"Peer user ID (session_search only): locate the private chat between the current user and this account exactly, independent of title/nickname/remark. Takes precedence over id/keyword. Use this when you know the target user's ID (e.g. the agent owner). Returns error 4004 when no such private chat exists."},keyword:{type:"string",description:"Search keyword."},sessionType:{type:"integer",enum:[1,2],description:"Filter by session type (session_search only): 1=private chat, 2=group chat. Omit to return all."},limit:{type:"integer",description:"Max results. message_history defaults to 1 when omitted."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"]},id:{type:"string"},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$"},keyword:{type:"string",maxLength:200},sessionType:{type:"integer",enum:[1,2]},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_audit_data",description:"Read connector-backed conversation audit replay data from Grix/AIBot: manifest, span pages, and content chunks.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"],description:"Audit read action. Start with get_manifest, then call list_spans and/or get_content_chunk based on the manifest."},auditId:{type:"string",description:"Audit ID returned by the audited turn metadata."},revision:{type:"integer",description:"Optional manifest revision to read. Usually use the revision returned by get_manifest."},cursor:{type:"string",description:"Opaque pagination cursor returned by list_spans or get_content_chunk."},limit:{type:"integer",description:"Max span items for list_spans."},contentId:{type:"string",description:"Content reference ID from manifest.content_refs (get_content_chunk only)."},maxBytes:{type:"integer",description:"Max UTF-8 bytes to return for get_content_chunk."}},required:["action","auditId"]},validation:{required:["action","auditId"],properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"]},auditId:{type:"string"},revision:{type:"integer",minimum:1},cursor:{type:"string",maxLength:4096},limit:{type:"integer",minimum:1,maximum:200},contentId:{type:"string"},maxBytes:{type:"integer",minimum:1,maximum:131072}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform. Do NOT use this to answer the inbound event you are currently handling \u2014 answer in plain text (or the reply tool where available) instead, otherwise the user receives duplicate messages.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_message_edit",description:"Edit the content of a message you (this agent) previously sent, in place. You can only edit your own message, and only a plain text/markdown one \u2014 card messages (approval cards, question cards, binding cards, status cards, etc.) can never be edited by this tool and are rejected by the server. Requires the owner to have granted the Edit Own Messages permission scope \u2014 if rejected for missing permission, surface the server error as-is; do not retry, do not fall back to grix_message_send, and tell the owner to grant the permission in the agent scope settings.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID the message belongs to."},msgId:{type:"string",description:"ID of the message to edit (must be a message you sent)."},content:{type:"string",description:"New content to replace the message with."}},required:["sessionId","msgId","content"]},validation:{required:["sessionId","msgId","content"],properties:{sessionId:{type:"string"},msgId:{type:"string"},content:{type:"string",maxLength:1e4}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. The link is reachable only inside the tailnet, so just send it as-is \u2014 no need to worry about or mention any link lifetime. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds. Leave unset to use the long default; set only if you deliberately want a short-lived link."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_file_upload",description:"Upload a local file to the Grix platform and send it as a media message in the target session. Supports images (jpg/png/webp/gif/bmp/heic/heif), videos (mp4/mov/m4v/webm/mkv/avi), documents (pdf/doc/docx/xls/xlsx/ppt/pptx/txt/md/csv/json/xml), and archives (zip/rar/7z/tar/gz). Max 50 MB. Use this instead of grix_file_link when the file should appear as a native attachment in the chat (visible inline for images/videos), rather than a tailnet download link.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file to upload."},session_id:{type:"string",description:"Target session ID to send the file to."},caption:{type:"string",description:"Optional text caption for the media message."},reply_to_message_id:{type:"string",description:"Optional message ID to quote/reply to."}},required:["file_path","session_id"]},validation:{required:["file_path","session_id"],properties:{file_path:{type:"string",maxLength:4096},session_id:{type:"string"},caption:{type:"string",maxLength:2e3},reply_to_message_id:{type:"string"}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Professional behavioral introduction describing the Agent purpose, responsibilities, operating expectations, and boundaries (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the display name and/or text introduction of one of your owner's agents, identified by its numeric agent ID. Provide agent_name, introduction, or both \u2014 at least one is required.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},agent_name:{type:"string",description:"New display name (max 100 characters, must be unique among the owner's agents)."},introduction:{type:"string",description:"New text introduction (max 3072 characters)."}},required:["agent_id"]},validation:{required:["agent_id"],properties:{agent_id:{type:"string"},agent_name:{type:"string",maxLength:100},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:`Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend creates a NEW private session between the owner and that agent for each dispatch (it does not reuse past sessions), binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session AS THE OWNER so the agent starts working. Because the task is delivered as the owner, write it in the owner's first-person voice and tone \u2014 phrase it the way the owner would speak directly to the agent (e.g. "\u5E2E\u6211\u2026", "\u4F60\u53BB\u2026"), NOT as a third-person relay or as yourself narrating on the owner's behalf. Provide a short title summarizing the core of the task \u2014 it becomes the new session's title; if omitted the backend derives one from the task text.`,inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},cwd:{type:"string",description:"Absolute working directory where the agent should do the work."},task:{type:"string",description:"Text description of the task to perform, written in the owner's first-person voice and tone \u2014 it is delivered into the session as the owner, so phrase it as the owner speaking directly to the agent, not as a third-person relay."},title:{type:"string",description:"Optional short title (a few words) summarizing the core of the task; used as the new session's title. If omitted, the backend derives a title from the task text."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4},title:{type:"string",maxLength:255}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Dispatch callbacks (report_dispatch_result) MUST use this tool, even when you are a member of the callback session: a message sent as yourself cannot quote-wake the dispatcher agent, and your membership is not checked. NEVER use this for your own ordinary replies in a conversation \u2014 that would make your words show up as the owner's message; reply normally (or use grix_message_send to send as yourself) for those. The owner must be a member of the target session. Requires the Send as Owner permission scope \u2014 if rejected for missing permission, surface the error as-is so the owner can grant it; do not fall back to grix_message_send. Optional quoted_message_id quotes a message in the same session (used by dispatch callbacks to wake the dispatcher agent).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."},quoted_message_id:{type:"string",description:"Optional message ID in the target session to quote/reply to.",pattern:"^[1-9]\\d*$"}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4},quoted_message_id:{type:"string",pattern:"^[1-9]\\d*$"}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). When session_id identifies one completed session, the entry also includes exactly one clean final_result; non-completed and list queries never include message content. Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_webhook_create",description:'Create a webhook endpoint for a session this agent belongs to. POSTing {"content":"..."} to the returned URL sends that text into the session as the owner and wakes the agent \u2014 use it for human/timer-driven scheduled or recurring triggers (cron, schtasks, launchd). Not for agent-to-agent dispatch callbacks.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID the webhook posts into (required). Must be a session this agent is a member of \u2014 normally the current chat_id."},expires_at:{type:"string",description:"Optional RFC3339 expiry; omit for a non-expiring endpoint."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"},expires_at:{type:"string"}}}},{name:"grix_webhook_list",description:"List the active webhook endpoints (with full URLs) of a session this agent belongs to. Call this BEFORE grix_webhook_create and reuse an existing endpoint instead of creating another one.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID (required). Must be a session this agent is a member of \u2014 normally the current chat_id."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_webhook_delete",description:"Delete a webhook endpoint by id (from grix_webhook_list / grix_webhook_create). Only endpoints of sessions this agent belongs to. Use when tearing down scheduled triggers or removing duplicate endpoints.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Webhook endpoint id (required, pass as string)."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"}}}},{name:"grix_widget_visitor_ban",description:"Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Widget visitor session ID (required)."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_egg_search",description:"Search the Grix egg market (published skill/persona packages the owner can hatch into a new agent or install into an existing one). Keyword matching is per-term AND over name + description + category, so search short keywords (one concept per call) rather than a whole sentence; omit keyword to browse a category. Results include id, name, description, category, install_count, can_create_agent, and existing_agent_client_types.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Short search keyword(s); terms are ANDed. Omit to list by category."},categoryId:{type:"string",description:"Optional egg category ID to restrict results."},locale:{type:"string",description:"Optional locale such as zh-CN or en; defaults to the owner's locale."},page:{type:"integer",description:"Page number, starting from 1."},pageSize:{type:"integer",description:"Items per page (1-50), default 20."}}},validation:{required:[],properties:{keyword:{type:"string",maxLength:200},categoryId:{type:"string"},locale:{type:"string",maxLength:16},page:{type:"integer",minimum:1},pageSize:{type:"integer",minimum:1,maximum:50}}}},{name:"grix_egg_get",description:"Get one Grix egg by id \u2014 full description, current version, and whether it can create a new agent or install into existing agents. Use after grix_egg_search to show details before the owner decides to hatch.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Egg ID from grix_egg_search."},locale:{type:"string",description:"Optional locale."},version:{type:"integer",description:"Optional specific version; defaults to the current one."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"},locale:{type:"string",maxLength:16},version:{type:"integer",minimum:1}}}},{name:"grix_skill_set",description:"Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (max 100 characters); unique within the owner. Used as the identifier and the synced directory name."},content:{type:"string",description:"Full SKILL.md text. Pass an empty string to delete the skill by name."}},required:["name","content"]},validation:{required:["name","content"],properties:{name:{type:"string",maxLength:100},content:{type:"string",maxLength:262144}}}},{name:"grix_skill_get",description:"Read one of your owner's custom skills by name (returns its full SKILL.md content), or list the owner's skill library when no name is given. Use this before editing to fetch current content, then call grix_skill_set with the revised text.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name to fetch. Omit to list all skills (names + versions) instead."}}},validation:{required:[],properties:{name:{type:"string",maxLength:100}}}}],a=[{name:"grix_reply",description:"Send your final reply \u2014 the conclusion the user is waiting for \u2014 to the specified session. This is the message the user treats as your answer; deliver your final result here, and use plain text only for brief progress notes while you work. Supports streaming in chunks; the frontend automatically aggregates them into one complete message. The connector quotes the message being answered automatically, so quoted_message_id is optional (set it only to quote a different earlier message).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],w={grix_dispatch_agent:"grix-agent-dispatch",grix_agent_update:"grix-agent-dispatch",grix_query:"grix-query",grix_audit_data:"grix-audit-data",grix_group:"grix-group",grix_message_send:"message-send",grix_message_unsend:"message-unsend",grix_message_edit:"message-edit",grix_admin:"grix-admin",grix_call_owner:"grix-owner-relay",grix_session_send:"grix-owner-relay",grix_chat_state_query:"grix-chat-state",grix_chat_state_update:"grix-chat-state",grix_status:"grix-chat-state",grix_access_control:"grix-access-control",grix_widget_visitor_ban:"grix-widget-visitor-ban",grix_webhook_create:"grix-scheduled-trigger",grix_webhook_list:"grix-scheduled-trigger",grix_webhook_delete:"grix-scheduled-trigger",grix_file_link:"tailnet-file-share"};function v(e){return` Before calling this tool, follow the \`${e}\` skill's procedure first; do not invoke this tool directly without going through that skill's guidance.`}for(const e of[...r,...a]){const t=w[e.name];t&&(e.description+=v(t))}const p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),x=new Set(r.map(e=>e.name)),k=new Set(a.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,S=/[A-Za-z0-9._-]+/;function Y(e){return!!(x.has(e)||k.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const q=[...r,...a],A=[...q,...p],X=new Map(A.map(e=>[e.name,e]));function J(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 g(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 g(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=g(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 ee(e){return c.has(e)}function te(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_message_edit":return N(t);case"grix_file_link":return C(t);case"grix_file_upload":return P(t);case"grix_admin":return Z(t);case"grix_call_owner":return U(t);case"grix_agent_update":return z(t);case"grix_dispatch_agent":return R(t);case"grix_session_send":return G(t);case"grix_chat_state_query":return B(t);case"grix_chat_state_update":return Q(t);case"grix_access_control":return W(t);case"grix_widget_visitor_ban":return F(t);case"grix_webhook_create":return $(t);case"grix_webhook_list":return{action:"webhook_list",params:{session_id:t.session_id}};case"grix_webhook_delete":return{action:"webhook_delete",params:{id:t.id}};case"grix_egg_search":return O(t);case"grix_egg_get":return M(t);case"grix_skill_set":return H(t);case"grix_skill_get":return K(t);default:throw new Error(`Unknown tool: ${e}`)}}const m={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search",search_favorite_sessions:"search_favorite_sessions"};function T(e){const t=String(e.action??""),n=m[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.peerId!=null&&(i.peer_id=e.peerId),e.keyword!=null&&(i.keyword=e.keyword),e.sessionType!=null&&(i.session_type=e.sessionType),e.limit!=null?i.limit=e.limit:t==="message_history"&&(i.limit=1),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}function O(e){const t={};return e.keyword!=null&&(t.keyword=e.keyword),e.categoryId!=null&&(t.category_id=e.categoryId),e.locale!=null&&(t.locale=e.locale),e.page!=null&&(t.page=e.page),e.pageSize!=null&&(t.page_size=e.pageSize),{action:"egg_search",params:t}}function M(e){const t={id:e.id};return e.locale!=null&&(t.locale=e.locale),e.version!=null&&(t.version=e.version),{action:"egg_get",params:t}}const h={get_manifest:"audit_get_manifest",list_spans:"audit_list_spans",get_content_chunk:"audit_get_content_chunk"};function D(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_audit_data action: ${t}`);const i={audit_id:e.auditId};return e.revision!=null&&(i.revision=e.revision),e.cursor!=null&&(i.cursor=e.cursor),e.limit!=null&&(i.limit=e.limit),e.contentId!=null&&(i.content_id=e.contentId),e.maxBytes!=null&&(i.max_bytes=e.maxBytes),{action:n,params:i}}const y={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function L(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function E(e){const t={session_id:e.sessionId,msg_type:e.msgType??1,content:e.content};return e.quotedMessageId!=null&&(t.quoted_message_id=e.quotedMessageId),e.threadId!=null&&(t.thread_id=e.threadId),{action:"send_msg",params:t}}function j(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function N(e){return{action:"message_edit",params:{session_id:e.sessionId,msg_id:e.msgId,content:e.content}}}function C(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 P(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 U(e){return{action:"call_owner",params:{session_id:e.session_id}}}function z(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 R(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 B(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 W(e){const t=String(e.action??""),n=V[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 F(e){return{action:"widget_visitor_ban",params:{session_id:e.session_id}}}function $(e){const t={session_id:e.session_id};return e.expires_at!=null&&(t.expires_at=e.expires_at),{action:"webhook_create",params:t}}function Q(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 H(e){return{action:"skill_set",params:{name:e.name,content:e.content},timeoutMs:3e4}}function K(e){const t={};return e.name!=null&&(t.name=e.name),{action:"skill_get",params:t}}function Z(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 ie=new Set([...Object.values(m),...Object.values(h),...Object.values(y),...Object.values(f),"send_msg","delete_msg","message_edit","file_link","file_upload","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update","claude_access_control","widget_visitor_ban","webhook_create","webhook_list","webhook_delete","skill_set","skill_get","egg_search","egg_get"]),V={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{V as ACCESS_CONTROL_ACTION_MAP,q as ALL_TOOLS,a as EVENT_TOOLS,A as EXPOSED_TOOLS,ie as PHASE1_INVOKE_ACTIONS,x as PHASE1_TOOL_NAMES,k as PHASE2_TOOL_NAMES,r as TOOLS,p as TOOL_ALIASES,X as TOOL_MAP,ee as isAlias,Y as isGrixInternalToolName,J as mapToolAlias,_ as normalizeEventToolArgs,te as toolCallToInvoke};
|
|
1
|
+
const r=[{name:"grix_query",description:"Search contacts, sessions, message history, messages, or favorited sessions in the Grix/AIBot platform. message_history defaults to the newest one clean text/approval-card message and excludes process/tool cards.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"],description:"Query action type. search_favorite_sessions returns the owner's favorited sessions (supports keyword filter)."},id:{type:"string",description:"Contact ID (contact_search) or Session ID (session_search)."},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$",description:"Peer user ID (session_search only): locate the private chat between the current user and this account exactly, independent of title/nickname/remark. Takes precedence over id/keyword. Use this when you know the target user's ID (e.g. the agent owner). Returns error 4004 when no such private chat exists."},keyword:{type:"string",description:"Search keyword."},sessionType:{type:"integer",enum:[1,2],description:"Filter by session type (session_search only): 1=private chat, 2=group chat. Omit to return all."},limit:{type:"integer",description:"Max results. message_history defaults to 1 when omitted."},offset:{type:"integer",description:"Result offset."},sessionId:{type:"string",description:"Session ID (message_history, message_search)."},beforeId:{type:"string",description:"Pagination cursor (message_history, message_search)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search","search_favorite_sessions"]},id:{type:"string"},peerId:{type:"string",pattern:"^[1-9]\\d{0,19}$"},keyword:{type:"string",maxLength:200},sessionType:{type:"integer",enum:[1,2]},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}}},{name:"grix_audit_data",description:"Read connector-backed conversation audit replay data from Grix/AIBot: manifest, span pages, and content chunks.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"],description:"Audit read action. Start with get_manifest, then call list_spans and/or get_content_chunk based on the manifest."},auditId:{type:"string",description:"Audit ID returned by the audited turn metadata."},revision:{type:"integer",description:"Optional manifest revision to read. Usually use the revision returned by get_manifest."},cursor:{type:"string",description:"Opaque pagination cursor returned by list_spans or get_content_chunk."},limit:{type:"integer",description:"Max span items for list_spans."},contentId:{type:"string",description:"Content reference ID from manifest.content_refs (get_content_chunk only)."},maxBytes:{type:"integer",description:"Max UTF-8 bytes to return for get_content_chunk."}},required:["action","auditId"]},validation:{required:["action","auditId"],properties:{action:{type:"string",enum:["get_manifest","list_spans","get_content_chunk"]},auditId:{type:"string"},revision:{type:"integer",minimum:1},cursor:{type:"string",maxLength:4096},limit:{type:"integer",minimum:1,maximum:200},contentId:{type:"string"},maxBytes:{type:"integer",minimum:1,maximum:131072}}}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"],description:"Group action type."},sessionId:{type:"string",description:"Group session ID."},name:{type:"string",description:"Group name (create)."},memberIds:{type:"array",items:{type:"string"},description:"Member IDs to add/remove."},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]},description:"Member types (1=user, 2=agent)."},memberId:{type:"string",description:"Target member ID."},role:{type:"integer",enum:[1,2],description:"New role (1=admin, 2=member)."},memberType:{type:"integer",description:"Member type."},allMembersMuted:{type:"boolean",description:"Whether to mute all members."},isSpeakMuted:{type:"boolean",description:"Whether member is muted."},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform. Do NOT use this to answer the inbound event you are currently handling \u2014 answer in plain text (or the reply tool where available) instead, otherwise the user receives duplicate messages.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Target session ID"},content:{type:"string",description:"Message content"},msgType:{type:"integer",description:"Message type (1=text, default 1)"},quotedMessageId:{type:"string",description:"Message ID to reply to"},threadId:{type:"string",description:"Thread ID for threaded reply"}},required:["sessionId","content"]},validation:{required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID"},msgId:{type:"string",description:"Message ID to unsend"}},required:["sessionId","msgId"]},validation:{required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}}},{name:"grix_message_edit",description:"Edit the content of a message you (this agent) previously sent, in place. You can only edit your own message, and only a plain text/markdown one \u2014 card messages (approval cards, question cards, binding cards, status cards, etc.) can never be edited by this tool and are rejected by the server. Requires the owner to have granted the Edit Own Messages permission scope \u2014 if rejected for missing permission, surface the server error as-is; do not retry, do not fall back to grix_message_send, and tell the owner to grant the permission in the agent scope settings.",inputSchema:{type:"object",properties:{sessionId:{type:"string",description:"Session ID the message belongs to."},msgId:{type:"string",description:"ID of the message to edit (must be a message you sent)."},content:{type:"string",description:"New content to replace the message with."}},required:["sessionId","msgId","content"]},validation:{required:["sessionId","msgId","content"],properties:{sessionId:{type:"string"},msgId:{type:"string"},content:{type:"string",maxLength:1e4}}}},{name:"grix_file_link",description:"Create a direct, tailnet-only download link for a local file on this host. Use this whenever the user asks you to send, share, give, or deliver a file that exists on the machine where you run (a report, log, build artifact, export, or any local path). It returns a ready-to-use Markdown link in the `markdown` field \u2014 include that exact Markdown link in your reply so the user can click and download the file directly over the shared Tailscale network. The link is reachable only inside the tailnet, so just send it as-is \u2014 no need to worry about or mention any link lifetime. The download link is HTTPS, served by a built-in self-signed CA. The result also returns `ca_install_url`: the first time you share a link with a user (or whenever their browser warns the cert is untrusted), also give them this CA install link so they can install and trust it once \u2014 after that all download links work without warnings. Requires this host to be on a tailnet (Tailscale running).",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file on this host to share with the user."},ttl_ms:{type:"integer",description:"Optional link lifetime in milliseconds. Leave unset to use the long default; set only if you deliberately want a short-lived link."}},required:["file_path"]},validation:{required:["file_path"],properties:{file_path:{type:"string",maxLength:4096},ttl_ms:{type:"integer",minimum:1e4,maximum:864e5}}}},{name:"grix_file_upload",description:"Upload a local file to the Grix platform and send it as a media message in the target session. Supports images (jpg/png/webp/gif/bmp/heic/heif), videos (mp4/mov/m4v/webm/mkv/avi), documents (pdf/doc/docx/xls/xlsx/ppt/pptx/txt/md/csv/json/xml), and archives (zip/rar/7z/tar/gz). Max 50 MB. Use this instead of grix_file_link when the file should appear as a native attachment in the chat (visible inline for images/videos), rather than a tailnet download link.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to a local file to upload."},session_id:{type:"string",description:"Target session ID to send the file to."},caption:{type:"string",description:"Optional text caption for the media message."},reply_to_message_id:{type:"string",description:"Optional message ID to quote/reply to."}},required:["file_path","session_id"]},validation:{required:["file_path","session_id"],properties:{file_path:{type:"string",maxLength:4096},session_id:{type:"string"},caption:{type:"string",maxLength:2e3},reply_to_message_id:{type:"string"}}}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"],description:"Admin action type."},agentName:{type:"string",description:"Agent name (create_agent)."},introduction:{type:"string",description:"Professional behavioral introduction describing the Agent purpose, responsibilities, operating expectations, and boundaries (create_agent)."},isMain:{type:"boolean",description:"Set as main agent (create_agent)."},agentId:{type:"string",description:"Agent ID (assign_category, rotate_api_key)."},categoryId:{type:"string",description:"Category ID (create_agent, update_category, assign_category)."},name:{type:"string",description:"Category name (create_category, update_category)."},parentId:{type:"string",description:"Parent category ID (create_category, update_category)."},sortOrder:{type:"integer",description:"Sort order (create_category, update_category)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}}},{name:"grix_call_owner",description:"Call your owner into this session to talk by voice. Use this when, during your work, you need to reach your owner \u2014 to discuss something or to get an approval/review. It sends the owner an offline notification; when they tap it they land directly in this conversation and a voice-brain call is started automatically. Requires the owner to have configured a voice brain. Rate-limited per session.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID to call the owner into."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_agent_update",description:"Update the display name and/or text introduction of one of your owner's agents, identified by its numeric agent ID. Provide agent_name, introduction, or both \u2014 at least one is required.",inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},agent_name:{type:"string",description:"New display name (max 100 characters, must be unique among the owner's agents)."},introduction:{type:"string",description:"New text introduction (max 3072 characters)."}},required:["agent_id"]},validation:{required:["agent_id"],properties:{agent_id:{type:"string"},agent_name:{type:"string",maxLength:100},introduction:{type:"string",maxLength:300}}}},{name:"grix_dispatch_agent",description:`Dispatch one of your owner's agents to do work in a given working directory. Provide the target agent numeric ID, the working directory, and a text description of the task. The backend creates a NEW private session between the owner and that agent for each dispatch (it does not reuse past sessions), binds the working directory when the agent type requires it (claude/codex/etc.), and sends the task into the session AS THE OWNER so the agent starts working. Because the task is delivered as the owner, write it in the owner's first-person voice and tone \u2014 phrase it the way the owner would speak directly to the agent (e.g. "\u5E2E\u6211\u2026", "\u4F60\u53BB\u2026"), NOT as a third-person relay or as yourself narrating on the owner's behalf. Provide a short title summarizing the core of the task \u2014 it becomes the new session's title; if omitted the backend derives one from the task text.`,inputSchema:{type:"object",properties:{agent_id:{type:"string",description:"Target agent's numeric ID, passed as a string."},cwd:{type:"string",description:"Absolute working directory where the agent should do the work."},task:{type:"string",description:"Text description of the task to perform, written in the owner's first-person voice and tone \u2014 it is delivered into the session as the owner, so phrase it as the owner speaking directly to the agent, not as a third-person relay."},title:{type:"string",description:"Optional short title (a few words) summarizing the core of the task; used as the new session's title. If omitted, the backend derives a title from the task text."}},required:["agent_id","cwd","task"]},validation:{required:["agent_id","cwd","task"],properties:{agent_id:{type:"string"},cwd:{type:"string",maxLength:4096},task:{type:"string",maxLength:1e4},title:{type:"string",maxLength:255}}}},{name:"grix_session_send",description:"Send a message into a session AS THE OWNER \u2014 it appears as if the owner sent it, NOT as you (the agent). Dispatch callbacks (report_dispatch_result) MUST use this tool, even when you are a member of the callback session: a message sent as yourself cannot quote-wake the dispatcher agent, and your membership is not checked. NEVER use this for your own ordinary replies in a conversation \u2014 that would make your words show up as the owner's message; reply normally (or use grix_message_send to send as yourself) for those. The owner must be a member of the target session. Requires the Send as Owner permission scope \u2014 if rejected for missing permission, surface the error as-is so the owner can grant it; do not fall back to grix_message_send. Optional quoted_message_id quotes a message in the same session (used by dispatch callbacks to wake the dispatcher agent).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID."},content:{type:"string",description:"Message content to send as the owner."},quoted_message_id:{type:"string",description:"Optional message ID in the target session to quote/reply to.",pattern:"^[1-9]\\d*$"}},required:["session_id","content"]},validation:{required:["session_id","content"],properties:{session_id:{type:"string"},content:{type:"string",maxLength:1e4},quoted_message_id:{type:"string",pattern:"^[1-9]\\d*$"}}}},{name:"grix_chat_state_query",description:"Query the chat-level task states across all of your owner's sessions (including direct and group chats). Returns one entry per session with a single mutually-exclusive state: running (working), waiting_approval (blocked on your owner to approve/deny), waiting_question (asked the owner a question, awaiting their reply), completed, failed, or idle (no task / stopped). When session_id identifies one completed session, the entry also includes exactly one clean final_result; non-completed and list queries never include message content. Also returns the session title (task_title) for easy identification. Supports pagination (page/page_size) and optional state filtering. Use this to see at a glance which chats are done, still running, or waiting on the owner.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"(Optional) Query a single session by its ID. Omit to return all sessions."},page:{type:"number",description:"(Optional) Page number, starting from 1. Defaults to 1 if omitted."},page_size:{type:"number",description:"(Optional) Number of items per page, max 100. Defaults to 10 if omitted."},state:{type:"string",description:"(Optional) Filter by a specific state: running, waiting_approval, waiting_question, completed, failed, or idle. Omit to return all states."}}},validation:{required:[],properties:{}}},{name:"grix_chat_state_update",description:"Manually update the task state of a specific chat session. Use this to mark a chat as completed, failed, idle, or any other state when you need to override it manually. The reason is written to stop_reason and is optional.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"The session ID whose state to update."},state:{type:"string",enum:["running","waiting_approval","waiting_question","completed","failed","idle"],description:"The new state to set. Must be one of: running, waiting_approval, waiting_question, completed, failed, idle."},reason:{type:"string",description:"(Optional) Reason for the state change, written to stop_reason."}}},validation:{required:["session_id","state"],properties:{}}},{name:"grix_access_control",description:"Manage sender access control: pair approval, allow/remove senders, set policy.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"],description:"Access control action type."},code:{type:"string",description:"Pairing code (required for pair_approve/pair_deny)."},sender_id:{type:"string",description:"Sender ID (required for allow_sender/remove_sender)."},policy:{type:"string",enum:["allowlist","open","disabled"],description:"Access policy (required for set_policy)."}},required:["action"]},validation:{required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}}},{name:"grix_webhook_create",description:'Create a webhook endpoint for a session this agent belongs to. POSTing {"content":"..."} to the returned URL sends that text into the session as the owner and wakes the agent \u2014 use it for human/timer-driven scheduled or recurring triggers (cron, schtasks, launchd). Not for agent-to-agent dispatch callbacks.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID the webhook posts into (required). Must be a session this agent is a member of \u2014 normally the current chat_id."},expires_at:{type:"string",description:"Optional RFC3339 expiry; omit for a non-expiring endpoint."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"},expires_at:{type:"string"}}}},{name:"grix_webhook_list",description:"List the active webhook endpoints (with full URLs) of a session this agent belongs to. Call this BEFORE grix_webhook_create and reuse an existing endpoint instead of creating another one.",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID (required). Must be a session this agent is a member of \u2014 normally the current chat_id."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_webhook_delete",description:"Delete a webhook endpoint by id (from grix_webhook_list / grix_webhook_create). Only endpoints of sessions this agent belongs to. Use when tearing down scheduled triggers or removing duplicate endpoints.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Webhook endpoint id (required, pass as string)."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"}}}},{name:"grix_widget_visitor_ban",description:"Ban a widget visitor session: blocks the session and adds its most recent init IP to the owner's global IP ban list (7-day expiry).",inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Widget visitor session ID (required)."}},required:["session_id"]},validation:{required:["session_id"],properties:{session_id:{type:"string"}}}},{name:"grix_egg_search",description:"Search the Grix egg market (published skill/persona packages the owner can hatch into a new agent or install into an existing one). Keyword matching is per-term AND over name + description + category, so search short keywords (one concept per call) rather than a whole sentence; omit keyword to browse a category. Results include id, name, description, category, install_count, can_create_agent, and existing_agent_client_types.",inputSchema:{type:"object",properties:{keyword:{type:"string",description:"Short search keyword(s); terms are ANDed. Omit to list by category."},categoryId:{type:"string",description:"Optional egg category ID to restrict results."},locale:{type:"string",description:"Optional locale such as zh-CN or en; defaults to the owner's locale."},page:{type:"integer",description:"Page number, starting from 1."},pageSize:{type:"integer",description:"Items per page (1-50), default 20."}}},validation:{required:[],properties:{keyword:{type:"string",maxLength:200},categoryId:{type:"string"},locale:{type:"string",maxLength:16},page:{type:"integer",minimum:1},pageSize:{type:"integer",minimum:1,maximum:50}}}},{name:"grix_egg_get",description:"Get one Grix egg by id \u2014 full description, current version, and whether it can create a new agent or install into existing agents. Use after grix_egg_search to show details before the owner decides to hatch.",inputSchema:{type:"object",properties:{id:{type:"string",description:"Egg ID from grix_egg_search."},locale:{type:"string",description:"Optional locale."},version:{type:"integer",description:"Optional specific version; defaults to the current one."}},required:["id"]},validation:{required:["id"],properties:{id:{type:"string"},locale:{type:"string",maxLength:16},version:{type:"integer",minimum:1}}}},{name:"grix_skill_set",description:"Create, update, or delete one of your owner's custom skills in the platform skill library. The skill library auto-syncs to every machine the owner runs an agent on (a per-machine `grix/skills` directory), so defining a skill here makes it available on all of them. A skill is a plain SKILL.md package (frontmatter + body), same standard as other Grix skills. Provide `name` and the full `content` (the SKILL.md text) to create or overwrite by name. To delete a skill, pass its `name` with an empty `content` string. How and when any agent actually loads/uses a synced skill is arranged by the owner (in a system prompt or in conversation); this tool only manages the library.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (max 100 characters); unique within the owner. Used as the identifier and the synced directory name."},content:{type:"string",description:"Full SKILL.md text. Pass an empty string to delete the skill by name."}},required:["name","content"]},validation:{required:["name","content"],properties:{name:{type:"string",maxLength:100},content:{type:"string",maxLength:262144}}}},{name:"grix_skill_get",description:"Read one of your owner's custom skills by name (returns its full SKILL.md content), or list the owner's skill library when no name is given. Use this before editing to fetch current content, then call grix_skill_set with the revised text.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name to fetch. Omit to list all skills (names + versions) instead."}}},validation:{required:[],properties:{name:{type:"string",maxLength:100}}}},{name:"grix_toolbar_set",description:`Declare this agent's own toolbar controls above the chat input (client_type "acp" only). Two kinds: "select" renders a dropdown, and the user's choice is delivered back to you as a grix://toolbar/select?item=<id>&option=<id> message; "info" renders a button that shows info_text when tapped and never calls back. Call it again with the full desired list to update, or with an empty array to remove everything. At most 6 items.`,inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Target session ID. Defaults to the session this ACP session is bound to."},items:{type:"array",description:"The complete toolbar, replacing whatever was declared before. Empty array clears it.",items:{type:"object",properties:{id:{type:"string",description:"Stable identifier, [a-z0-9_]+, max 32 chars."},kind:{type:"string",enum:["select","info"]},label:{type:"string",description:"Control label, max 20 characters."},value:{type:"string",description:"select only: the currently selected option id."},options:{type:"array",description:"select only: 1-20 options.",items:{type:"object",properties:{id:{type:"string"},label:{type:"string",description:"Max 40 characters."}},required:["id","label"]}},info_title:{type:"string",description:"info only: dialog title, max 20 characters. Defaults to label."},info_text:{type:"string",description:"info only: dialog body, max 500 characters."}},required:["id","kind","label"]}}},required:["items"]},validation:{required:["items"],properties:{session_id:{type:"string"},items:{type:"array"}}}}],a=[{name:"grix_reply",description:"Send your final reply \u2014 the conclusion the user is waiting for \u2014 to the specified session. This is the message the user treats as your answer; deliver your final result here, and use plain text only for brief progress notes while you work. Supports streaming in chunks; the frontend automatically aggregates them into one complete message. The connector quotes the message being answered automatically, so quoted_message_id is optional (set it only to quote a different earlier message).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"Associated event ID from the inbound event."},session_id:{type:"string",description:"Target session ID."},text:{type:"string",description:"Reply text content."},quoted_message_id:{type:"string",description:"Quoted message ID (optional)."},is_final:{type:"boolean",description:"Whether this is a stage-final reply. Advisory only \u2014 does not trigger event completion; completion is handled by the complete tool or Stop hook."}},required:["session_id","text"]},validation:{required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}}},{name:"grix_complete",description:"Mark event processing as complete, notifying the backend that no more replies are expected.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to complete."},status:{type:"string",enum:["responded","canceled","failed"],description:"Completion status."},msg:{type:"string",description:"Additional note (optional)."}},required:["event_id","status"]},validation:{required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}}},{name:"grix_event_ack",description:"Acknowledge event receipt (usually done automatically by the Dispatcher; agents typically do not need to call this manually).",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The event ID to acknowledge."},session_id:{type:"string",description:"Session ID."}},required:["event_id"]},validation:{required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}}},{name:"grix_composing",description:'Set the "typing" indicator status for a session.',inputSchema:{type:"object",properties:{session_id:{type:"string",description:"Session ID."},active:{type:"boolean",description:"true = typing, false = stopped."},event_id:{type:"string",description:"Associated event ID (optional)."}},required:["session_id","active"]},validation:{required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}}},{name:"grix_status",description:"Query the Grix connection status of the current MCP session.",inputSchema:{type:"object",properties:{}},validation:{required:[],properties:{}}}],w={grix_dispatch_agent:"grix-agent-dispatch",grix_agent_update:"grix-agent-dispatch",grix_query:"grix-query",grix_audit_data:"grix-audit-data",grix_group:"grix-group",grix_message_send:"message-send",grix_message_unsend:"message-unsend",grix_message_edit:"message-edit",grix_admin:"grix-admin",grix_call_owner:"grix-owner-relay",grix_session_send:"grix-owner-relay",grix_chat_state_query:"grix-chat-state",grix_chat_state_update:"grix-chat-state",grix_status:"grix-chat-state",grix_access_control:"grix-access-control",grix_widget_visitor_ban:"grix-widget-visitor-ban",grix_webhook_create:"grix-scheduled-trigger",grix_webhook_list:"grix-scheduled-trigger",grix_webhook_delete:"grix-scheduled-trigger",grix_file_link:"tailnet-file-share"};function v(e){return` Before calling this tool, follow the \`${e}\` skill's procedure first; do not invoke this tool directly without going through that skill's guidance.`}for(const e of[...r,...a]){const t=w[e.name];t&&(e.description+=v(t))}const p=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},final:{type:"boolean",description:"Advisory flag only. It does not complete the event; completion is handled by complete tool or Stop hook."}},required:["chat_id","event_id","text"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string"},code:{type:"string"}},required:["event_id","status"]}}],c=new Set(p.map(e=>e.name)),x=new Set(r.map(e=>e.name)),k=new Set(a.map(e=>e.name)),I=/([A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)/,S=/[A-Za-z0-9._-]+/;function X(e){return!!(x.has(e)||k.has(e)||c.has(e)||e.startsWith("mcp__grix"))}const q=[...r,...a],A=[...q,...p],J=new Map(A.map(e=>[e.name,e]));function ee(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 g(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 g(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=g(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 te(e){return c.has(e)}function ie(e,t){switch(e){case"grix_query":return O(t);case"grix_audit_data":return L(t);case"grix_group":return E(t);case"grix_message_send":return j(t);case"grix_message_unsend":return N(t);case"grix_message_edit":return C(t);case"grix_file_link":return P(t);case"grix_file_upload":return z(t);case"grix_admin":return V(t);case"grix_call_owner":return U(t);case"grix_agent_update":return R(t);case"grix_dispatch_agent":return G(t);case"grix_session_send":return B(t);case"grix_chat_state_query":return W(t);case"grix_chat_state_update":return H(t);case"grix_access_control":return F(t);case"grix_widget_visitor_ban":return $(t);case"grix_webhook_create":return Q(t);case"grix_webhook_list":return{action:"webhook_list",params:{session_id:t.session_id}};case"grix_webhook_delete":return{action:"webhook_delete",params:{id:t.id}};case"grix_egg_search":return M(t);case"grix_egg_get":return D(t);case"grix_skill_set":return K(t);case"grix_skill_get":return Z(t);case"grix_toolbar_set":return T(t);default:throw new Error(`Unknown tool: ${e}`)}}function T(e){const t={items:e.items??[]};return e.session_id!=null&&(t.session_id=e.session_id),{action:"toolbar_set",params:t}}const m={contact_search:"contact_search",session_search:"session_search",message_history:"message_history",message_search:"message_search",search_favorite_sessions:"search_favorite_sessions"};function O(e){const t=String(e.action??""),n=m[t];if(!n)throw new Error(`Unknown grix_query action: ${t}`);const i={};return e.id!=null&&(i.id=e.id),e.peerId!=null&&(i.peer_id=e.peerId),e.keyword!=null&&(i.keyword=e.keyword),e.sessionType!=null&&(i.session_type=e.sessionType),e.limit!=null?i.limit=e.limit:t==="message_history"&&(i.limit=1),e.offset!=null&&(i.offset=e.offset),e.sessionId!=null&&(i.session_id=e.sessionId),e.beforeId!=null&&(i.before_id=e.beforeId),{action:n,params:i}}function M(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 D(e){const t={id:e.id};return e.locale!=null&&(t.locale=e.locale),e.version!=null&&(t.version=e.version),{action:"egg_get",params:t}}const h={get_manifest:"audit_get_manifest",list_spans:"audit_list_spans",get_content_chunk:"audit_get_content_chunk"};function L(e){const t=String(e.action??""),n=h[t];if(!n)throw new Error(`Unknown grix_audit_data action: ${t}`);const i={audit_id:e.auditId};return e.revision!=null&&(i.revision=e.revision),e.cursor!=null&&(i.cursor=e.cursor),e.limit!=null&&(i.limit=e.limit),e.contentId!=null&&(i.content_id=e.contentId),e.maxBytes!=null&&(i.max_bytes=e.maxBytes),{action:n,params:i}}const y={create:"group_create",detail:"group_detail_read",leave:"group_leave_self",add_members:"group_member_add",remove_members:"group_member_remove",update_member_role:"group_member_role_update",update_all_members_muted:"group_all_members_muted_update",update_member_speaking:"group_member_speaking_update",dissolve:"group_dissolve"};function E(e){const t=String(e.action??""),n=y[t];if(!n)throw new Error(`Unknown grix_group action: ${t}`);const i={};return e.sessionId!=null&&(i.session_id=e.sessionId),e.name!=null&&(i.name=e.name),e.memberIds!=null&&(i.member_ids=e.memberIds),e.memberTypes!=null&&(i.member_types=e.memberTypes),e.memberId!=null&&(i.member_id=e.memberId),e.role!=null&&(i.role=e.role),e.memberType!=null&&(i.member_type=e.memberType),e.allMembersMuted!=null&&(i.all_members_muted=e.allMembersMuted),e.isSpeakMuted!=null&&(i.is_speak_muted=e.isSpeakMuted),e.canSpeakWhenAllMuted!=null&&(i.can_speak_when_all_muted=e.canSpeakWhenAllMuted),{action:n,params:i}}function j(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 N(e){return{action:"delete_msg",params:{session_id:e.sessionId,msg_id:e.msgId}}}function C(e){return{action:"message_edit",params:{session_id:e.sessionId,msg_id:e.msgId,content:e.content}}}function P(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 U(e){return{action:"call_owner",params:{session_id:e.session_id}}}function R(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 G(e){return{action:"dispatch_agent",params:{agent_id:e.agent_id,cwd:e.cwd,task:e.task,title:e.title},timeoutMs:75e3}}function B(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 W(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 F(e){const t=String(e.action??""),n=Y[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 $(e){return{action:"widget_visitor_ban",params:{session_id:e.session_id}}}function Q(e){const t={session_id:e.session_id};return e.expires_at!=null&&(t.expires_at=e.expires_at),{action:"webhook_create",params:t}}function H(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 K(e){return{action:"skill_set",params:{name:e.name,content:e.content},timeoutMs:3e4}}function Z(e){const t={};return e.name!=null&&(t.name=e.name),{action:"skill_get",params:t}}function V(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 ne=new Set([...Object.values(m),...Object.values(h),...Object.values(y),...Object.values(f),"send_msg","delete_msg","message_edit","file_link","file_upload","call_owner","agent_introduction_update","dispatch_agent","session_send","chat_state_query","chat_state_update","claude_access_control","widget_visitor_ban","webhook_create","webhook_list","webhook_delete","skill_set","skill_get","egg_search","egg_get","toolbar_set"]),Y={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"};export{Y as ACCESS_CONTROL_ACTION_MAP,q as ALL_TOOLS,a as EVENT_TOOLS,A as EXPOSED_TOOLS,ne as PHASE1_INVOKE_ACTIONS,x as PHASE1_TOOL_NAMES,k as PHASE2_TOOL_NAMES,r as TOOLS,p as TOOL_ALIASES,J as TOOL_MAP,te as isAlias,X as isGrixInternalToolName,ee as mapToolAlias,_ as normalizeEventToolArgs,ie as toolCallToInvoke};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{parentPort as c,workerData as l}from"node:worker_threads";import*as r from"@sentry/node";const i=c;if(!i)throw new Error("sentry-worker must run inside a worker thread");const d=e=>i.postMessage(e),s=l;r.init({dsn:s.dsn,release:s.release,environment:s.environment,sendDefaultPii:!1,initialScope:{tags:{component:"grix-connector"}},defaultIntegrations:!1,tracesSampleRate:0});const a=new Map;r.getClient()?.on("afterSendEvent",(e,t)=>{const n=e.event_id===void 0?void 0:a.get(e.event_id);if(n===void 0)return;a.delete(e.event_id);const o=t.statusCode;d({type:"sent",key:n,ok:o!==void 0&&o>=200&&o<300})}),i.on("message",e=>{if(e.type==="capture"){const t=new Error(e.error.message);t.name=e.error.name,e.error.stack&&(t.stack=e.error.stack);const n=r.captureException(t,{level:e.level,tags:e.tags,contexts:e.contexts,fingerprint:e.fingerprint});a.set(n,e.key)}else e.type==="close"&&r.close(e.timeoutMs).finally(()=>d({type:"closed"}))}),d({type:"ready"});
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{log as
|
|
1
|
+
import{Worker as w}from"node:worker_threads";import{log as a}from"../log/logger.js";import{resolveClientVersion as k}from"../util/client-version.js";import{PersistentSentryDeduplicator as h}from"./sentry-event-dedup.js";let i="off",c=null,m="",f=Promise.resolve();const d=[],l=new Map;let R=1,u=null;const g=new h,I="https://e8e202d7625372b1314b3ff4e85a7ff9@o119262.ingest.us.sentry.io/4511410543329280";function _(){if(!process.env.GRIX_SENTRY_DISABLE)return process.env.SENTRY_DSN||process.env.GRIX_SENTRY_DSN||I}function M(){return i!=="off"||(f=v()),f}function C(){return f}function v(){const e=_();if(!e)return Promise.resolve();const r=performance.now();m=`grix-connector@${k()}`;const t={dsn:e,release:m,environment:process.env.SENTRY_ENVIRONMENT||process.env.NODE_ENV||"production"};return new Promise(n=>{let o;try{o=new w(new URL("./sentry-worker.js",import.meta.url),{workerData:t})}catch(s){a.warn("sentry",`Sentry worker \u542F\u52A8\u5931\u8D25\uFF0C\u9519\u8BEF\u4E0A\u62A5\u5DF2\u7981\u7528: ${p(s)}`),n();return}c=o,i="starting",o.on("message",s=>{if(s.type==="ready"){if(c!==o)return;i="ready",o.unref(),a.info("sentry",`ready in ${Math.round(performance.now()-r)}ms (worker)`);for(const y of d.splice(0))N(y);n()}else if(s.type==="sent"){const y=l.get(s.key);if(!y)return;l.delete(s.key),g.recordSendResult(y,s.ok)}else s.type==="closed"&&u?.()}),o.on("error",s=>{E(o,`Sentry worker \u51FA\u9519\uFF0C\u9519\u8BEF\u4E0A\u62A5\u5DF2\u7981\u7528: ${p(s)}`),n()}),o.on("exit",s=>{E(o,`Sentry worker \u5DF2\u9000\u51FA\uFF08code ${s}\uFF09\uFF0C\u9519\u8BEF\u4E0A\u62A5\u5DF2\u7981\u7528`),n()})})}function E(e,r){u?.(),c===e&&(c=null,i="off",d.length=0,l.clear(),a.warn("sentry",r))}function N(e){try{c?.postMessage(e)}catch(r){l.delete(e.key),a.error("sentry",`\u63D0\u4EA4 Sentry \u4E8B\u4EF6\u5931\u8D25: ${p(r)}`)}}function x(e){const r=[];for(const t of(e??"").split(`
|
|
2
|
+
`)){if(r.length>=50)break;if(t.length>1024)continue;const n=/^\s*at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?$/.exec(t);n&&r.push({function:n[1],filename:n[2],lineno:Number(n[3]),colno:Number(n[4])})}return r.reverse()}function S(e){const r={name:String(e.error.name),message:String(e.error.message),stack:typeof e.error.stack=="string"?e.error.stack:void 0},t={release:m,platform:"node",level:e.level,fingerprint:e.fingerprint,exception:{values:[{type:r.name,value:r.message,stacktrace:{frames:x(r.stack)}}]}};if(!L(t))return!1;const n={...e,type:"capture",key:R++,error:r};return l.set(n.key,t),i==="ready"?N(n):d.push(n),!0}function p(e){return e instanceof Error?e.message:String(e)}function A(e){if(typeof e.message=="string"&&e.message)return e.message;const r=e.exception?.values;if(!r?.length)return"";const t=r[0],n=typeof t?.value=="string"?t.value:"",o=typeof t?.type=="string"?t.type:"";return n||o}function W(){return i==="ready"}function L(e,r,t=g){const n=A(e);return n&&T(n)?null:t.shouldSend(e)?e:null}const D=new Set(["INSTALL_FAILED","INSTALL_TIMEOUT","FALLBACK_EXHAUSTED","PREREQ_INSTALL_FAILED","VERIFICATION_FAILED","ENVIRONMENT_UNSUPPORTED","INTERNAL"]);function $(e){if(e.ok)return!1;const r=e.error?.code;return!!r&&D.has(r)}function U(e){if(i==="off"||!$(e))return;const r=e.error?.code??"INTERNAL",t=e.error?.message??"unknown install failure",n=e.environment;try{S({kind:"install",level:"error",error:new Error(`Agent install failed [${e.agentType}/${r}]: ${t}`),tags:{agent_type:e.agentType,error_code:r,phase:e.phase},contexts:{install:{agentType:e.agentType,code:r,phase:e.phase,durationMs:e.durationMs,os:n?.platform,osVersion:n?.osVersion,arch:n?.arch,nodeVersion:n?.nodeVersion,npmVersion:n?.npmVersion,isDocker:n?.isDocker,isCI:n?.isCI,outputTail:(e.output??"").slice(-2e3)}},fingerprint:["agent-install-failure",e.agentType,r]})&&a.info("sentry",`\u5DF2\u63D0\u4EA4\u5B89\u88C5\u5931\u8D25\u7ED9 Sentry worker: ${e.agentType}/${r}`)}catch(o){a.error("sentry",`\u4E0A\u62A5\u5B89\u88C5\u5931\u8D25\u65F6\u51FA\u9519: ${p(o)}`)}}const b=[/No config files found/,/spawn wmic ENOENT/i];function T(e){const r=e instanceof Error?e.message:String(e);return b.some(t=>t.test(r))}function B(e){const r=e instanceof Error?e.message:String(e);return/spawn wmic ENOENT/i.test(r)}function Y(e,r){if(i!=="off")try{if(T(e))return;const t=e instanceof Error?e:new Error(String(e));S({kind:r,level:"fatal",error:t,tags:{crash_type:r}})&&a.info("sentry",`\u5DF2\u63D0\u4EA4\u5D29\u6E83\u7ED9 Sentry worker: ${r}`)}catch(t){a.error("sentry",`\u4E0A\u62A5\u5D29\u6E83\u65F6\u51FA\u9519: ${p(t)}`)}}async function X(e=2e3){await f;const r=c;i!=="ready"||!r||(c=null,i="off",await new Promise(t=>{const n=setTimeout(t,e);u=()=>{clearTimeout(n),t()};try{const o={type:"close",timeoutMs:e};r.postMessage(o)}catch{u()}}),u=null,r.terminate().catch(()=>{}))}export{X as closeSentry,A as extractEventMessage,L as filterSentryEvent,M as initSentry,T as isNonReportable,W as isSentryEnabled,B as isWindowsWmicProbeError,Y as reportFatal,U as reportInstallFailure,$ as shouldReportInstallFailure,C as whenSentryReady};
|
package/dist/grix.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import b from"node:path";import{writeFileSync as
|
|
3
|
-
Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),E.version&&(console.log(
|
|
2
|
+
import b from"node:path";import{writeFileSync as se}from"node:fs";import{Manager as $e}from"./manager.js";import{ensureGrixDirs as Le,initLogger as Ue,getLoggerStatus as He,log as i,installProcessLogRotation as Oe,setConsoleOutput as ce}from"./core/log/index.js";import{HealthServer as Fe,bindPortOrFail as le}from"./core/runtime/index.js";import{writePidFile as Ne,removePidFile as T,readDaemonPid as de}from"./core/runtime/index.js";import{killOrphanedClaudeProcesses as Ge}from"./core/runtime/index.js";import{resolveRuntimePaths as Y}from"./core/config/index.js";import{supportsNonMitmRelayConfig as Me}from"./core/config/provider-env.js";import{ServiceManager as pe}from"./service/service-manager.js";import{RESTART_EXIT_CODE as Be,UNHEALTHY_EXIT_CODE as je,markManagedStop as ge,resolveLauncherStore as q,writeCrashNote as We}from"./core/upgrade/version-store.js";import{migrateWindowsWrapper as J}from"./service/windows-wrapper-migration.js";const Ke=300*1e3;import{killProcessesByCommandLine as Xe,isWindowsElevated as Ve,shouldSweepWindowsWrappers as Ye,getProcessCommandLine as qe}from"./service/process-control.js";import{buildServiceID as Je}from"./service/service-paths.js";import{isInsideManagedDaemonTree as ze}from"./service/platform-adapter.js";import{acquireDaemonLock as Qe,createHealthzHolderProbe as Ze,DaemonAlreadyServingError as et,findLiveLockHolder as tt,isLockHolderSameProcess as rt,readDaemonLock as z,releaseDaemonLock as N}from"./runtime/daemon-lock.js";import{writeDaemonStatus as B,removeDaemonStatus as nt}from"./runtime/service-state.js";import{readRecentDuplicateDaemonRecord as ot,recordDuplicateDaemon as at}from"./runtime/duplicate-daemon.js";import{createStartupPhaseTimer as it}from"./runtime/startup-phases.js";import{RemoteAdminSettingsStore as st}from"./core/config/remote-admin-settings.js";import{AdminServer as ct,generateToken as lt,writeTokenFile as dt}from"./core/admin/index.js";import{initSentry as pt,whenSentryReady as Q,closeSentry as j,reportFatal as Z,isWindowsWmicProbeError as gt}from"./core/observability/sentry.js";import{initProxyManager as mt,getProxyManager as h,relayHostsForClientType as me,RelayStateStore as ut,SecretStore as ft}from"./core/proxy/index.js";import{stopProxyIfNobodyNeedsIt as ue,switchAgentRelayMode as ee,reconcileRelayStates as fe}from"./core/proxy/relay-orchestration.js";import{setAgentRelayCredential as he,RelayCredentialError as ye}from"./core/proxy/relay-credential.js";import{RelayFetchError as we,RelayToggleCoordinator as ht}from"./core/proxy/relay-credential-fetch.js";import{applyHermesProfileRelay as yt,clearHermesProfileRelay as wt,discoverHermesProfiles as St}from"./core/proxy/hermes-profile-relay.js";import{disableRelayCredential as Rt,enableRelayCredential as Et,RelayRouteError as Se}from"./core/proxy/relay-credential-router.js";import{upgradeStatusSnapshot as Dt}from"./core/upgrade/npm-upgrader.js";import{resolveClientVersion as At}from"./core/util/client-version.js";import{parseCliArgs as vt}from"./core/util/cli-args.js";const{command:I,flags:E,unknownFlags:te}=vt(process.argv.slice(2));if(te.length>0&&(console.error(`Unknown option${te.length>1?"s":""}: ${te.map(t=>`--${t}`).join(", ")}
|
|
3
|
+
Run \`grix-connector --help\` to see the supported commands and options.`),process.exit(1)),E.version&&(console.log(At()),process.exit(0)),E.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
|
|
4
4
|
|
|
5
5
|
Usage: grix-connector <command> [options]
|
|
6
6
|
|
|
@@ -34,10 +34,10 @@ Examples:
|
|
|
34
34
|
grix-connector start # Start as system service
|
|
35
35
|
grix-connector status # Check service status
|
|
36
36
|
grix-connector restart # Restart the service
|
|
37
|
-
`),process.exit(0)),I==="reload"){const t=
|
|
37
|
+
`),process.exit(0)),I==="reload"){const t=de();t||(console.error("reload failed: daemon is not running (no pid file)"),process.exit(1));try{process.kill(t,0)}catch{console.error(`reload failed: daemon process ${t} is not running (stale pid file)`),process.exit(1)}{const s=z(Y().daemonLockFile);s&&s.pid===t&&!rt(s)&&(console.error(`reload failed: pid ${t} has been reused by another process (stale pid file)`),process.exit(1))}try{process.kill(t,"SIGHUP"),console.log(JSON.stringify({ok:!0,signaled:t},null,2)),process.exit(0)}catch(s){console.error(`reload failed: ${s instanceof Error?s.message:s}`),process.exit(1)}}const Re="detected connector-managed session; restart delegated to launchd, this process may be terminated by the restart",Ee=["start","stop","restart","status"];if(I&&Ee.includes(I)){process.platform==="win32"&&["start","stop","restart"].includes(I)&&!Ve()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
|
|
38
38
|
using Startup folder auto-start instead. For full Task Scheduler integration,
|
|
39
|
-
right-click the terminal and select "Run as administrator".`);const t=Y(),s=E["config-dir"]??(E.profile?b.join(t.configDir,E.profile):void 0),g=b.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),f=new
|
|
40
|
-
Valid commands: ${
|
|
41
|
-
`),i.warn("main",s);const g=q();g&&
|
|
42
|
-
`),i.error("main",t.message.replace(/\n/g," \u2014 ")),K("port_bind",t.message);const s=z(c.daemonLockFile);s?.pid===process.pid&&s.instance_id===$&&await B(c.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:$,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await j(),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(1)}async function k(t){if(W)return;W=!0,i.info("main",`Received ${t}, shutting down...`),G.markShuttingDown();const s=q();s&&pe(s);const g=t==="upgrade-switch"?Me:t==="launcher-unhealthy"?Be:null,f=g??2,l=t==="uncaughtException"||t==="unhandledRejection",m=setTimeout(()=>{i.error("main","Shutdown timed out, forcing exit"),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(f)},1e4);try{await o.stop(),await h()?.stop().catch(()=>{}),await x.stop(),await G.stop(),await j(),await N(c.daemonLockFile),await rt(c.daemonStatusFile).catch(()=>{}),clearTimeout(m),T(),i.info("main","Shutdown complete"),process.exit(g??(l?2:0))}catch(D){i.error("main",`Shutdown error: ${D}`),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(f)}}async function Pt(){const t=at();t.lap("boot"),$e(),Le(),He(c.stdoutLogFile,c.stderrLogFile),se(!1);try{$=(await ze(c.daemonLockFile,c.rootDir,2,{isHolderServing:Qe(c.dataDir)})).instance_id}catch(e){e instanceof Ze&&bt(e.holder),console.error(e instanceof Error?e.message:e),process.exit(1)}if(t.lap("lock"),Ve()){const e=qe(c.rootDir,"win32");await Ke(e,{platform:"win32"})}t.lap("sweep"),await dt(),Fe(),t.lap("sentry"),i.info("main",`grix-connector starting (PID ${process.pid})`);try{Ne({info:e=>i.info("main",e),warn:e=>i.warn("main",e)})}catch(e){i.warn("main",`orphan Claude cleanup failed: ${e instanceof Error?e.message:String(e)}`)}t.lap("orphan"),await B(c.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:$,updated_at:Date.now()}),process.env.GRIX_LAUNCHER_MANAGED!=="1"&&await new de({cliPath:b.resolve(process.argv[1]??""),nodePath:process.execPath}).adoptLauncher(c.rootDir).then(e=>{e&&i.info("main","Service definition now points at the version-store launcher")}).catch(e=>{i.warn("main",`Launcher adoption failed: ${e instanceof Error?e.message:e}`)}),t.lap("adopt");const s=await J({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).catch(e=>(i.warn("main",`Windows wrapper migration failed: ${e instanceof Error?e.message:e}`),null));if(t.lap("migrate"),s?.handoffStarted)return;process.platform==="win32"&&process.env.GRIX_LAUNCHER_MANAGED!=="1"&&setInterval(()=>{W||J({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).then(n=>{n.handoffStarted&&i.info("main","Wrapper handoff started by the periodic migration retry")}).catch(n=>{i.warn("main",`Periodic wrapper migration failed: ${n instanceof Error?n.message:n}`)})},We).unref();const g=parseInt(E["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await ce({label:"health",port:g,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:n=>G.start(n)});e&&await Ae(e)}const f=b.join(c.dataDir,"health-port");ie(f,String(g),"utf-8"),t.lap("health"),process.on("SIGINT",()=>k("SIGINT")),process.on("SIGTERM",()=>k("SIGTERM")),process.on("SIGHUP",()=>{W||(i.info("main","Received SIGHUP, reloading config..."),o.reload().then(e=>i.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>i.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let l="",m=0,D;process.on("uncaughtException",e=>{const n=e instanceof Error?e.stack??e.message:String(e);n===l?(m++,(m<=3||m%100===0)&&i.error("main",`Uncaught exception (x${m}): ${n}`)):(m>3&&i.error("main",`Previous exception repeated ${m} times total`),l=n,m=1,i.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),D||(D=setTimeout(()=>{m>3&&i.error("main",`Previous exception repeated ${m} times total`),l="",m=0,D=void 0},1e4).unref())),!ve(e)&&(Q(e,"uncaughtException"),K("uncaughtException",e),k("uncaughtException"))}),process.on("unhandledRejection",e=>{i.error("main",`Unhandled rejection: ${e}`),!ve(e)&&(Q(e,"unhandledRejection"),K("unhandledRejection",e),k("unhandledRejection"))});const L=gt(c.dataDir);await L.clearRuntimeState();const _=new mt(b.join(c.dataDir,"proxy","relay-state.json"),{warn:e=>i.warn("main",e)}),M=new ut(b.join(c.dataDir,"secrets"));o.setRelaySecretResolver(e=>M.get(e));const U={hasDirectConfig:e=>o.hasDirectRelayConfig(e),clearDirectConfig:(e,n)=>o.setAgentProvider(e,void 0,{deferRestart:n?.deferRestart===!0})},X=()=>({pm:h(),agents:o,direct:U,store:_,secrets:M}),H=L.getRelayAgents();if(H.length>0)try{await L.startIfAnyRelayAgent(),i.info("main",`mitm proxy started for relay agents: ${H.join(", ")} (per-agent; others stay direct)`)}catch(e){i.error("main",`mitm proxy FAILED to start (relay agents: ${H.join(", ")}): ${e}. These agents will REFUSE to start (they must not silently fall back to your own account). Fix the proxy or turn relay off for them. Other agents are unaffected.`)}else i.info("main",'mitm proxy idle (no agent has Grix relay enabled; enable: PUT /api/proxy/agents/<name>/enabled {"enabled":true})');let O=null;if(h()){const e=async()=>{const r=h();r&&await me(r,o)&&i.info("main","no agent uses Grix relay anymore; mitm proxy stopped")};o.setRelayEnvSettledHandler(()=>{const r=h();r&&ue(r,U,_),e().catch(a=>{i.warn("main",`failed to stop idle mitm proxy: ${a}`)})});const n=()=>{const r=h(),a=r.getRelayAgents(),p=r.getDegradedRelayAgents();return{enabled:a.length>0,relayAgents:a,staleRelayAgents:o.getAgentsWithStaleRelayEnv(),degradedRelayAgents:p,relayState:_.list(),runtime:r.getRuntimeInfo(),config:r.getConfigSnapshot()}},d=new ft,A={status:()=>n(),setAgentRelay:async(r,a,p)=>{const u=h(),R={info:w=>i.info("main",w),warn:w=>i.warn("main",w)},S=w=>o.getAgentsStatus().find(v=>v.name===w),y=S(r);if(!a){d.cancel(r);const w=await d.runExclusive(r,async()=>{const v=await Z(X(),R,{agentName:r,target:"off"});return{...n(),restarted:v.restarted,busy:v.busy,...v.pending?{pending:v.pending}:{}}});return o.reportRelayStateLocalChange(r),w}if(!y)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});if(!Ge(y.clientType)&&ge(y.clientType).length===0)throw Object.assign(new Error(`client type "${y.clientType??""}" does not support Grix relay`),{code:"UNSUPPORTED_CLIENT_TYPE"});const F=await d.runExclusive(r,async()=>{const w=d.epoch(r),v=S(r);if(!v)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});const ne=String(v.agentId??"").trim();if(!ne)throw new he(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const oe=d.waitCancelled(r,w);let C;try{C=await Promise.race([o.fetchRelayCredential(r,{model:p}),oe.promise.then(()=>{throw new ye("relay enable cancelled by a later disable","CANCELLED")})])}finally{oe.dispose()}d.assertCurrent(r,w);const{restarted:Pe,busy:Ie,pending:ae}=await fe(u,o,S,R,{localName:r,agentId:ne,virtualKey:C.apiKey,anthropicBaseUrl:C.anthropicBaseUrl,openaiBaseUrl:C.openaiBaseUrl,model:p,...C.directRelay!==void 0?{directRelay:C.directRelay}:{}},(ke,_e,Ce)=>o.setAgentProvider(ke,_e,{deferRestart:Ce?.deferRestart===!0}),{store:_,secrets:M,direct:U});if(d.epoch(r)!==w)throw await Z(X(),R,{agentName:r,target:"off"}),new ye("relay enable cancelled by a later disable","CANCELLED");return{...n(),restarted:Pe,busy:Ie,...ae?{pending:ae}:{}}});return o.reportRelayStateLocalChange(r),F},setAgentRelayCredential:async(r,a)=>{const p=h(),u=o.getAgentsStatus(),{restarted:R,busy:S,pending:y}=await fe(p,o,P=>u.find(F=>F.name===P),{info:P=>i.info("main",P),warn:P=>i.warn("main",P)},{localName:r,agentId:a.agentId,virtualKey:a.virtualKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}},(P,F,w)=>o.setAgentProvider(P,F,{deferRestart:w?.deferRestart===!0}),{store:_,secrets:M,direct:U});return o.reportRelayStateLocalChange(r),{...n(),restarted:R,busy:S,...y?{pending:y}:{}}},disableAll:async()=>{const r=h(),a={info:u=>i.info("main",u),warn:u=>i.warn("main",u)},p=new Set([...r.getRelayAgents(),...o.getDirectRelayConfigAgentNames(),...Object.keys(_.list())]);for(const u of p)d.cancel(u);for(const u of p)await d.runExclusive(u,()=>Z(X(),a,{agentName:u,target:"off"})),o.reportRelayStateLocalChange(u);return await me(r,o),n()},setRoute:async(r,a)=>{const p=a,u=h();return u.setRoute({routeKey:r,targetBaseUrl:p.targetBaseUrl,...p.headers?{headers:p.headers}:{},...p.model?{model:p.model}:{},...p.modelMap?{modelMap:p.modelMap}:{},...p.passthrough?{passthrough:!0}:{},...p.relayOnly?{relayOnly:!0}:{},...p.codexResponsesToChat?{codexResponsesToChat:!0}:{}}),await u.persistConfig(),n()},deleteRoute:async r=>{const a=h();a.deleteRoute(r),await a.persistConfig()},setDefaultRoute:async r=>{const a=h();return a.setDefaultRouteKey(r),await a.persistConfig(),n()},setInterceptHosts:async r=>{const a=h();return a.setInterceptHosts(r),await a.persistConfig(),n()}};O=A,x.setProxyHandler(A),o.setRelayStateApplier({getLocalState:r=>{const a=h();if(a.getRelayAgents().includes(r)){const u=o.getAgentsStatus().find(y=>y.name===r),R=`grix-gateway-${String(u?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:a.getConfigSnapshot().routes?.find(y=>y.routeKey===R)?.model?.trim()||void 0}}const p=o.getAgentProvider(r);return p?.api_key?.trim()?{enabled:!0,model:p.model?.trim()||void 0}:{enabled:!1}},applyEnable:async(r,a,p)=>{const u=o.getAgentsStatus().find(y=>y.name===r),R=String(u?.agentId??"").trim();if(!R)throw new he(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const S=await A.setAgentRelayCredential(r,{agentId:R,virtualKey:a.apiKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:p??a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}});return{restarted:S.restarted===!0,busy:S.busy===!0,...S.pending?{pending:S.pending}:{}}},applyDisable:async r=>{const a=await A.setAgentRelay(r,!1);return{restarted:a.restarted===!0,busy:a.busy===!0,...a.pending?{pending:a.pending}:{}}}})}G.setStatusProvider(()=>o.getAgentsStatus()),G.setMetaProvider(()=>{const e=o.getAgentsStatus(),n=Ue();return{ws:{connected:e.filter(d=>d.wsConnected===!0).length,total:e.length},upgrade:Et(),log:{dir:n.logDir,file:n.logFile,primary_dir:n.primaryLogDir,degraded:n.degraded,using_fallback:n.usingFallback,...n.degradeReason?{degrade_reason:n.degradeReason}:{}}}});const V=new it(b.join(c.dataDir,"remote-admin.json"));if(o.setRemoteAdminGate(()=>V.isEnabled()),t.lap("setup"),await o.start(vt),t.lap("agents"),H.length>0){const e=o.getAgentsStatus();for(const n of H){const d=e.find(r=>r.name===n);if(!d)continue;const A=ge(d.clientType);A.length!==0&&await L.setAgentRelayEnabled(n,!0,{relayHosts:A}).catch(r=>{i.warn("main",`failed to reconcile relay hosts for "${n}": ${r}`)})}}ue(L,U,_);const te=parseInt(E["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);x.setAgentHandler({list:()=>o.getAgentsStatus(),add:e=>o.addAgent(e),remove:e=>o.removeAgent(e),restart:e=>o.restartAgent(e),reload:()=>o.reload()}),x.setUpgradeHandler({check:()=>o.checkUpgrade(),trigger:()=>o.triggerUpgrade()}),x.setProbeHandler({probeAll:e=>o.probeAll(e),probeOne:(e,n)=>o.probeOne(e,n)}),x.setRemoteAdminHandler({isEnabled:()=>V.isEnabled(),setEnabled:e=>{const n=V.setEnabled(e);return i.info("main",`remote admin ${n?"enabled":"disabled"} via admin API`),n}}),x.setInstallHandler({listInstallable:()=>o.listInstallable(),installAgent:e=>o.installAgent(e),getInstallProgress:e=>o.getInstallProgress(e)});const re={listAgents:()=>o.getAgentsStatus(),enableManaged:(e,n,d)=>{if(!O)throw new we(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return O.setAgentRelayCredential(e,{agentId:n,virtualKey:d.virtualKey,anthropicBaseUrl:d.anthropicBaseUrl,openaiBaseUrl:d.openaiBaseUrl,model:d.model,...d.directRelay!==void 0?{directRelay:d.directRelay}:{}})},disableManaged:e=>{if(!O)throw new we(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return O.setAgentRelay(e,!1)},enableHermesProfile:(e,n)=>ht({agentId:e,baseUrl:n.openaiBaseUrl??"",virtualKey:n.virtualKey,model:n.model??""}),disableHermesProfile:e=>yt(e)};x.setRelayCredentialHandler({listHermesProfiles:()=>wt(),enable:async(e,n)=>{const{target:d,result:A}=await Rt(re,e,n);return{target:d,...A}},disable:async e=>{const{target:n,result:d}=await St(re,e);return{target:n,...d}}});{const e=await ce({label:"admin",port:te,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:n=>x.start(n)});e&&await Ae(e)}const be=b.join(c.dataDir,"admin-token"),xe=b.join(c.dataDir,"admin-port");lt(be,Ee),ie(xe,String(te),"utf-8"),t.lap("admin"),await B(c.daemonStatusFile,{state:"running",pid:process.pid,instance_id:$,updated_at:Date.now()}),i.info("main",`startup phases: ${t.summary()}`),process.send&&process.send("ready"),i.info("main","grix-connector ready")}Pt().catch(async t=>{const s=`Fatal: ${t instanceof Error?t.stack??t.message:String(t)}`;try{process.stderr.write(`${s}
|
|
43
|
-
`)}catch{}
|
|
39
|
+
right-click the terminal and select "Run as administrator".`);const t=Y(),s=E["config-dir"]??(E.profile?b.join(t.configDir,E.profile):void 0),g=b.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),f=new pe({cliPath:g,nodePath:process.execPath});try{let l;switch(I){case"start":{const m=await f.healLegacyState({rootDir:t.rootDir});(await f.status({rootDir:t.rootDir})).installed?l=await f.start({rootDir:t.rootDir}):l=await f.install({rootDir:t.rootDir,configDir:s??m.configDir}),m.healed&&(l={...l,legacy_heal:m.reasons})}break;case"stop":l=await f.stop({rootDir:t.rootDir});break;case"restart":if(!(await f.status({rootDir:t.rootDir})).installed)l=await f.install({rootDir:t.rootDir,configDir:s});else{const D=process.platform==="darwin"&&ze();D&&console.warn(Re),l=await f.restart({rootDir:t.rootDir}),D&&(l={...l,mode:"kickstart",note:Re})}break;case"status":l=await f.status({rootDir:t.rootDir});{const m=ot(t.dataDir);m&&(l={...l,duplicate_daemon:m})}break}console.log(JSON.stringify(l,null,2)),process.exit(0)}catch(l){console.error(`${I} failed: ${l instanceof Error?l.message:l}`),process.exit(1)}}else I&&(console.error(`Unknown command: ${I}
|
|
40
|
+
Valid commands: ${Ee.join(", ")}`),process.exit(1));const c=Y(),bt=E["config-dir"]??(E.profile?`${c.configDir}/${E.profile}`:void 0),o=new $e({requestGracefulShutdown:t=>{k(t)},onTransactionSettled:()=>{J({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).then(t=>{t.handoffStarted&&i.info("main","Wrapper handoff started after the upgrade transaction settled")}).catch(t=>{i.warn("main",`Post-upgrade wrapper migration failed: ${t instanceof Error?t.message:t}`)})}}),G=new Fe,De=lt(),x=new ct(De);let W=!1,$;function K(t,s){const g=q(),f=process.env.GRIX_LAUNCHER_VERSION;if(!g||!f)return;const l=s instanceof Error?s.stack??s.message:String(s);We(g,f,`${t}: ${l}`)}function Ae(t,s){at(c.dataDir,{holder_pid:t.pid,holder_command_line:t.command_line,yielded_pid:process.pid,yielded_command_line:qe(process.pid)}),process.stderr.write(`${s}
|
|
41
|
+
`),i.warn("main",s);const g=q();g&&ge(g)}function xt(t){const s=`another grix-connector daemon (pid ${t.pid}) is already serving ${c.rootDir} and answering /healthz; this process yields instead of taking over. A second supervisor is starting duplicate daemons \u2014 check for a leftover wrapper, a duplicate logon task, or a desktop app launching its own copy.`;Ae(t,s),process.exit(0)}function Pt(t,s){const g=`${s.label} \u7AEF\u53E3 ${s.port} \u5DF2\u88AB\u5360\u7528\uFF0C\u4E14 daemon \u9501\u4ECD\u7531\u53E6\u4E00\u4E2A\u5B58\u6D3B\u5B9E\u4F8B (pid ${t.pid}) \u6301\u6709\uFF1A\u672C\u8FDB\u7A0B\u6309\u7BA1\u7406\u6027\u9000\u51FA\u8BA9\u4F4D\uFF0C\u4E0D\u8BA1\u4E3A\u5D29\u6E83\u3002 \u8FD9\u53F0\u673A\u5668\u4E0A\u6709\u7B2C\u4E8C\u4E2A supervisor \u5728\u91CD\u590D\u62C9\u8D77 daemon\u2014\u2014\u68C0\u67E5\u6B8B\u7559\u7684 wrapper\u3001\u91CD\u590D\u7684\u767B\u5F55\u4EFB\u52A1\uFF0C\u6216\u684C\u9762\u7AEF\u53E6\u8D77\u7684\u526F\u672C\u3002`;Ae(t,g),de()===process.pid&&T(),process.exit(0)}async function ve(t){if(t.kind==="in_use"){const g=tt(c.daemonLockFile);g&&(await j(),Pt(g,t))}process.stderr.write(t.message+`
|
|
42
|
+
`),i.error("main",t.message.replace(/\n/g," \u2014 ")),K("port_bind",t.message);const s=z(c.daemonLockFile);s?.pid===process.pid&&s.instance_id===$&&await B(c.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:$,updated_at:Date.now(),reason:`port_bind_${t.kind}:${t.label}:${t.port}`}).catch(()=>{}),await j(),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(1)}async function k(t){if(W)return;W=!0,i.info("main",`Received ${t}, shutting down...`),G.markShuttingDown();const s=q();s&&ge(s);const g=t==="upgrade-switch"?Be:t==="launcher-unhealthy"?je:null,f=g??2,l=t==="uncaughtException"||t==="unhandledRejection",m=setTimeout(()=>{i.error("main","Shutdown timed out, forcing exit"),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(f)},1e4);try{await o.stop(),await h()?.stop().catch(()=>{}),await x.stop(),await G.stop(),await j(),await N(c.daemonLockFile),await nt(c.daemonStatusFile).catch(()=>{}),clearTimeout(m),T(),i.info("main","Shutdown complete"),process.exit(g??(l?2:0))}catch(D){i.error("main",`Shutdown error: ${D}`),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(f)}}async function It(){const t=it();t.lap("boot"),Le(),Ue(),Oe(c.stdoutLogFile,c.stderrLogFile),ce(!1);try{$=(await Qe(c.daemonLockFile,c.rootDir,2,{isHolderServing:Ze(c.dataDir)})).instance_id}catch(e){e instanceof et&&xt(e.holder),console.error(e instanceof Error?e.message:e),process.exit(1)}if(t.lap("lock"),Ye()){const e=Je(c.rootDir,"win32");await Xe(e,{platform:"win32"})}t.lap("sweep"),pt(),Ne(),t.lap("pidfile"),i.info("main",`grix-connector starting (PID ${process.pid})`);try{Ge({info:e=>i.info("main",e),warn:e=>i.warn("main",e)})}catch(e){i.warn("main",`orphan Claude cleanup failed: ${e instanceof Error?e.message:String(e)}`)}t.lap("orphan"),await B(c.daemonStatusFile,{state:"starting",pid:process.pid,instance_id:$,updated_at:Date.now()}),process.env.GRIX_LAUNCHER_MANAGED!=="1"&&await new pe({cliPath:b.resolve(process.argv[1]??""),nodePath:process.execPath}).adoptLauncher(c.rootDir).then(e=>{e&&i.info("main","Service definition now points at the version-store launcher")}).catch(e=>{i.warn("main",`Launcher adoption failed: ${e instanceof Error?e.message:e}`)}),t.lap("adopt");const s=await J({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).catch(e=>(i.warn("main",`Windows wrapper migration failed: ${e instanceof Error?e.message:e}`),null));if(t.lap("migrate"),s?.handoffStarted)return;process.platform==="win32"&&process.env.GRIX_LAUNCHER_MANAGED!=="1"&&setInterval(()=>{W||J({rootDir:c.rootDir,requestGracefulShutdown:()=>{k("wrapper-migration")}}).then(n=>{n.handoffStarted&&i.info("main","Wrapper handoff started by the periodic migration retry")}).catch(n=>{i.warn("main",`Periodic wrapper migration failed: ${n instanceof Error?n.message:n}`)})},Ke).unref();const g=parseInt(E["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);{const e=await le({label:"health",port:g,envVar:"GRIX_HEALTH_PORT",cliFlag:"health-port",start:n=>G.start(n)});e&&await ve(e)}const f=b.join(c.dataDir,"health-port");se(f,String(g),"utf-8"),t.lap("health"),process.on("SIGINT",()=>k("SIGINT")),process.on("SIGTERM",()=>k("SIGTERM")),process.on("SIGHUP",()=>{W||(i.info("main","Received SIGHUP, reloading config..."),o.reload().then(e=>i.info("main",`reload done: ${JSON.stringify(e)}`)).catch(e=>i.error("main",`reload failed: ${e instanceof Error?e.message:e}`)))});let l="",m=0,D;process.on("uncaughtException",e=>{const n=e instanceof Error?e.stack??e.message:String(e);n===l?(m++,(m<=3||m%100===0)&&i.error("main",`Uncaught exception (x${m}): ${n}`)):(m>3&&i.error("main",`Previous exception repeated ${m} times total`),l=n,m=1,i.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),D||(D=setTimeout(()=>{m>3&&i.error("main",`Previous exception repeated ${m} times total`),l="",m=0,D=void 0},1e4).unref())),!be(e)&&(Q().then(()=>Z(e,"uncaughtException")),K("uncaughtException",e),k("uncaughtException"))}),process.on("unhandledRejection",e=>{i.error("main",`Unhandled rejection: ${e}`),!be(e)&&(Q().then(()=>Z(e,"unhandledRejection")),K("unhandledRejection",e),k("unhandledRejection"))});const L=mt(c.dataDir);await L.clearRuntimeState();const _=new ut(b.join(c.dataDir,"proxy","relay-state.json"),{warn:e=>i.warn("main",e)}),M=new ft(b.join(c.dataDir,"secrets"));o.setRelaySecretResolver(e=>M.get(e));const U={hasDirectConfig:e=>o.hasDirectRelayConfig(e),clearDirectConfig:(e,n)=>o.setAgentProvider(e,void 0,{deferRestart:n?.deferRestart===!0})},X=()=>({pm:h(),agents:o,direct:U,store:_,secrets:M}),H=L.getRelayAgents();if(H.length>0)try{await L.startIfAnyRelayAgent(),i.info("main",`mitm proxy started for relay agents: ${H.join(", ")} (per-agent; others stay direct)`)}catch(e){i.error("main",`mitm proxy FAILED to start (relay agents: ${H.join(", ")}): ${e}. These agents will REFUSE to start (they must not silently fall back to your own account). Fix the proxy or turn relay off for them. Other agents are unaffected.`)}else i.info("main",'mitm proxy idle (no agent has Grix relay enabled; enable: PUT /api/proxy/agents/<name>/enabled {"enabled":true})');let O=null;if(h()){const e=async()=>{const r=h();r&&await ue(r,o)&&i.info("main","no agent uses Grix relay anymore; mitm proxy stopped")};o.setRelayEnvSettledHandler(()=>{const r=h();r&&fe(r,U,_),e().catch(a=>{i.warn("main",`failed to stop idle mitm proxy: ${a}`)})});const n=()=>{const r=h(),a=r.getRelayAgents(),p=r.getDegradedRelayAgents();return{enabled:a.length>0,relayAgents:a,staleRelayAgents:o.getAgentsWithStaleRelayEnv(),degradedRelayAgents:p,relayState:_.list(),runtime:r.getRuntimeInfo(),config:r.getConfigSnapshot()}},d=new ht,A={status:()=>n(),setAgentRelay:async(r,a,p)=>{const u=h(),R={info:w=>i.info("main",w),warn:w=>i.warn("main",w)},S=w=>o.getAgentsStatus().find(v=>v.name===w),y=S(r);if(!a){d.cancel(r);const w=await d.runExclusive(r,async()=>{const v=await ee(X(),R,{agentName:r,target:"off"});return{...n(),restarted:v.restarted,busy:v.busy,...v.pending?{pending:v.pending}:{}}});return o.reportRelayStateLocalChange(r),w}if(!y)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});if(!Me(y.clientType)&&me(y.clientType).length===0)throw Object.assign(new Error(`client type "${y.clientType??""}" does not support Grix relay`),{code:"UNSUPPORTED_CLIENT_TYPE"});const F=await d.runExclusive(r,async()=>{const w=d.epoch(r),v=S(r);if(!v)throw Object.assign(new Error(`Agent "${r}" not found`),{code:"NOT_FOUND"});const oe=String(v.agentId??"").trim();if(!oe)throw new ye(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const ae=d.waitCancelled(r,w);let C;try{C=await Promise.race([o.fetchRelayCredential(r,{model:p}),ae.promise.then(()=>{throw new we("relay enable cancelled by a later disable","CANCELLED")})])}finally{ae.dispose()}d.assertCurrent(r,w);const{restarted:Ie,busy:ke,pending:ie}=await he(u,o,S,R,{localName:r,agentId:oe,virtualKey:C.apiKey,anthropicBaseUrl:C.anthropicBaseUrl,openaiBaseUrl:C.openaiBaseUrl,model:p,...C.directRelay!==void 0?{directRelay:C.directRelay}:{}},(_e,Ce,Te)=>o.setAgentProvider(_e,Ce,{deferRestart:Te?.deferRestart===!0}),{store:_,secrets:M,direct:U});if(d.epoch(r)!==w)throw await ee(X(),R,{agentName:r,target:"off"}),new we("relay enable cancelled by a later disable","CANCELLED");return{...n(),restarted:Ie,busy:ke,...ie?{pending:ie}:{}}});return o.reportRelayStateLocalChange(r),F},setAgentRelayCredential:async(r,a)=>{const p=h(),u=o.getAgentsStatus(),{restarted:R,busy:S,pending:y}=await he(p,o,P=>u.find(F=>F.name===P),{info:P=>i.info("main",P),warn:P=>i.warn("main",P)},{localName:r,agentId:a.agentId,virtualKey:a.virtualKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}},(P,F,w)=>o.setAgentProvider(P,F,{deferRestart:w?.deferRestart===!0}),{store:_,secrets:M,direct:U});return o.reportRelayStateLocalChange(r),{...n(),restarted:R,busy:S,...y?{pending:y}:{}}},disableAll:async()=>{const r=h(),a={info:u=>i.info("main",u),warn:u=>i.warn("main",u)},p=new Set([...r.getRelayAgents(),...o.getDirectRelayConfigAgentNames(),...Object.keys(_.list())]);for(const u of p)d.cancel(u);for(const u of p)await d.runExclusive(u,()=>ee(X(),a,{agentName:u,target:"off"})),o.reportRelayStateLocalChange(u);return await ue(r,o),n()},setRoute:async(r,a)=>{const p=a,u=h();return u.setRoute({routeKey:r,targetBaseUrl:p.targetBaseUrl,...p.headers?{headers:p.headers}:{},...p.model?{model:p.model}:{},...p.modelMap?{modelMap:p.modelMap}:{},...p.passthrough?{passthrough:!0}:{},...p.relayOnly?{relayOnly:!0}:{},...p.codexResponsesToChat?{codexResponsesToChat:!0}:{}}),await u.persistConfig(),n()},deleteRoute:async r=>{const a=h();a.deleteRoute(r),await a.persistConfig()},setDefaultRoute:async r=>{const a=h();return a.setDefaultRouteKey(r),await a.persistConfig(),n()},setInterceptHosts:async r=>{const a=h();return a.setInterceptHosts(r),await a.persistConfig(),n()}};O=A,x.setProxyHandler(A),o.setRelayStateApplier({getLocalState:r=>{const a=h();if(a.getRelayAgents().includes(r)){const u=o.getAgentsStatus().find(y=>y.name===r),R=`grix-gateway-${String(u?.clientType??"").trim().toLowerCase()}`;return{enabled:!0,model:a.getConfigSnapshot().routes?.find(y=>y.routeKey===R)?.model?.trim()||void 0}}const p=o.getAgentProvider(r);return p?.api_key?.trim()?{enabled:!0,model:p.model?.trim()||void 0}:{enabled:!1}},applyEnable:async(r,a,p)=>{const u=o.getAgentsStatus().find(y=>y.name===r),R=String(u?.agentId??"").trim();if(!R)throw new ye(`local agent "${r}" has no agent_id registered`,"MISSING_AGENT_ID");const S=await A.setAgentRelayCredential(r,{agentId:R,virtualKey:a.apiKey,anthropicBaseUrl:a.anthropicBaseUrl,openaiBaseUrl:a.openaiBaseUrl,model:p??a.model,...a.directRelay!==void 0?{directRelay:a.directRelay}:{}});return{restarted:S.restarted===!0,busy:S.busy===!0,...S.pending?{pending:S.pending}:{}}},applyDisable:async r=>{const a=await A.setAgentRelay(r,!1);return{restarted:a.restarted===!0,busy:a.busy===!0,...a.pending?{pending:a.pending}:{}}}})}G.setStatusProvider(()=>o.getAgentsStatus()),G.setMetaProvider(()=>{const e=o.getAgentsStatus(),n=He();return{ws:{connected:e.filter(d=>d.wsConnected===!0).length,total:e.length},upgrade:Dt(),log:{dir:n.logDir,file:n.logFile,primary_dir:n.primaryLogDir,degraded:n.degraded,using_fallback:n.usingFallback,...n.degradeReason?{degrade_reason:n.degradeReason}:{}}}});const V=new st(b.join(c.dataDir,"remote-admin.json"));if(o.setRemoteAdminGate(()=>V.isEnabled()),t.lap("setup"),await o.start(bt),t.lap("agents"),H.length>0){const e=o.getAgentsStatus();for(const n of H){const d=e.find(r=>r.name===n);if(!d)continue;const A=me(d.clientType);A.length!==0&&await L.setAgentRelayEnabled(n,!0,{relayHosts:A}).catch(r=>{i.warn("main",`failed to reconcile relay hosts for "${n}": ${r}`)})}}fe(L,U,_);const re=parseInt(E["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);x.setAgentHandler({list:()=>o.getAgentsStatus(),add:e=>o.addAgent(e),remove:e=>o.removeAgent(e),restart:e=>o.restartAgent(e),reload:()=>o.reload()}),x.setUpgradeHandler({check:()=>o.checkUpgrade(),trigger:()=>o.triggerUpgrade()}),x.setProbeHandler({probeAll:e=>o.probeAll(e),probeOne:(e,n)=>o.probeOne(e,n)}),x.setRemoteAdminHandler({isEnabled:()=>V.isEnabled(),setEnabled:e=>{const n=V.setEnabled(e);return i.info("main",`remote admin ${n?"enabled":"disabled"} via admin API`),n}}),x.setInstallHandler({listInstallable:()=>o.listInstallable(),installAgent:e=>o.installAgent(e),getInstallProgress:e=>o.getInstallProgress(e)});const ne={listAgents:()=>o.getAgentsStatus(),enableManaged:(e,n,d)=>{if(!O)throw new Se(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return O.setAgentRelayCredential(e,{agentId:n,virtualKey:d.virtualKey,anthropicBaseUrl:d.anthropicBaseUrl,openaiBaseUrl:d.openaiBaseUrl,model:d.model,...d.directRelay!==void 0?{directRelay:d.directRelay}:{}})},disableManaged:e=>{if(!O)throw new Se(`agent "${e}" needs the MITM proxy for relay, but it is not running`,"PROXY_UNAVAILABLE");return O.setAgentRelay(e,!1)},enableHermesProfile:(e,n)=>yt({agentId:e,baseUrl:n.openaiBaseUrl??"",virtualKey:n.virtualKey,model:n.model??""}),disableHermesProfile:e=>wt(e)};x.setRelayCredentialHandler({listHermesProfiles:()=>St(),enable:async(e,n)=>{const{target:d,result:A}=await Et(ne,e,n);return{target:d,...A}},disable:async e=>{const{target:n,result:d}=await Rt(ne,e);return{target:n,...d}}});{const e=await le({label:"admin",port:re,envVar:"GRIX_ADMIN_PORT",cliFlag:"admin-port",start:n=>x.start(n)});e&&await ve(e)}const xe=b.join(c.dataDir,"admin-token"),Pe=b.join(c.dataDir,"admin-port");dt(xe,De),se(Pe,String(re),"utf-8"),t.lap("admin"),await B(c.daemonStatusFile,{state:"running",pid:process.pid,instance_id:$,updated_at:Date.now()}),i.info("main",`startup phases: ${t.summary()}`),process.send&&process.send("ready"),i.info("main","grix-connector ready")}It().catch(async t=>{const s=`Fatal: ${t instanceof Error?t.stack??t.message:String(t)}`;try{process.stderr.write(`${s}
|
|
43
|
+
`)}catch{}ce(!1),i.error("main",s),K("startup",t);const g=z(c.daemonLockFile);g?.pid===process.pid&&g.instance_id===$&&await B(c.daemonStatusFile,{state:"failed",pid:process.pid,instance_id:g.instance_id,updated_at:Date.now(),reason:"startup_error"}).catch(()=>{}),await Q(),Z(t,"startup"),await j(),N(c.daemonLockFile).catch(()=>{}),T(),process.exit(1)});const kt=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function be(t){return gt(t)?!0:t instanceof Error&&"code"in t?kt.has(t.code):!1}
|
package/dist/log.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as
|
|
2
|
-
`)},error(o,r,...
|
|
1
|
+
import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as i}from"node:path";import{homedir as m}from"node:os";const n=i(m(),".grix"),s={base:n,config:i(n,"config"),log:i(n,"log"),data:i(n,"data")};function S(){for(const o of Object.values(s))f(o)||l(o,{recursive:!0})}let a=null;function $(){const o=new Date().toISOString().slice(0,10),r=i(s.log,`grix-acp-${o}.log`);a=g(r,{flags:"a"})}function c(){return new Date().toISOString().slice(11,19)}const u={info(o,r,...t){const e=`${c()} [${o}] ${r}${t.length?" "+t.map(String).join(" "):""}`;console.log(e),a?.write(e+`
|
|
2
|
+
`)},error(o,r,...t){const e=`${c()} [${o}] ERROR ${r}${t.length?" "+t.map(String).join(" "):""}`;console.error(e),a?.write(e+`
|
|
3
3
|
`)}};export{s as GRIX_PATHS,S as ensureGrixDirs,$ as initLogger,u as log};
|