grix-connector 2.0.1 → 2.0.3

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.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: grix-group
3
+ description: Use the typed `grix_group` tool for Grix group lifecycle and membership operations. Trigger when users ask to create, inspect, leave, update, or dissolve groups, or when these operations fail with scope or permission errors.
4
+ trigger: 当用户要创建、查看、退出、更新或解散群组,或群成员/禁言权限相关操作时
5
+ ---
6
+
7
+ # Grix Group
8
+
9
+ Use the `grix_group` tool for Grix group lifecycle and membership management.
10
+
11
+ ## Tool contract
12
+
13
+ Call `grix_group` with one `action`:
14
+
15
+ - `create` — create a group. Requires `name`; optionally seed members with
16
+ `memberIds` + matching `memberTypes` (1=user, 2=agent).
17
+ - `detail` — get group details. Requires `sessionId`.
18
+ - `leave` — leave the group. Requires `sessionId`.
19
+ - `dissolve` — dissolve the group (owner/admin only). Requires `sessionId`.
20
+ - `add_members` / `remove_members` — requires `sessionId`, `memberIds`, and
21
+ `memberTypes` aligned by index.
22
+ - `update_member_role` — requires `sessionId`, `memberId`, `role` (1=admin,
23
+ 2=member).
24
+ - `update_all_members_muted` — requires `sessionId`, `allMembersMuted`.
25
+ - `update_member_speaking` — requires `sessionId`, `memberId`, `isSpeakMuted`,
26
+ and optionally `canSpeakWhenAllMuted`.
27
+
28
+ ## Rules
29
+
30
+ 1. `memberIds` and `memberTypes` are parallel arrays — keep them the same length
31
+ and order.
32
+ 2. Any action except `create` needs the target group's `sessionId`; resolve it
33
+ with `grix_query` (`session_search`) first if unknown.
34
+ 3. Scope or permission errors usually mean the current agent is not an admin of
35
+ that group — surface the exact error and required role, don't retry blindly.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: grix-owner-relay
3
+ description: Act on the owner's behalf in a session — send a message as the owner (`grix_session_send`), or call the owner into the current session for a voice talk/approval (`grix_call_owner`). Trigger when the user asks to speak as the owner in a session, or when you need to reach the owner to discuss or get approval.
4
+ trigger: 当需要以 owner 身份在某会话发言、或把 owner 叫进当前会话语音沟通/审批时
5
+ ---
6
+
7
+ # Grix Owner Relay
8
+
9
+ Interact with sessions on the owner's behalf, or pull the owner in.
10
+
11
+ ## Speak as the owner — `grix_session_send`
12
+
13
+ Send a message into a session **as the owner** (not as yourself). The owner must
14
+ be a member of the target session.
15
+
16
+ - `session_id` (required) — target session ID.
17
+ - `content` (required) — message text to send as the owner (max 10000 chars).
18
+
19
+ To send as yourself (the agent) instead, use the `message-send` skill
20
+ (`grix_message_send`).
21
+
22
+ ## Call the owner in — `grix_call_owner`
23
+
24
+ Bring the owner into a session for a voice conversation — use this when you need
25
+ to discuss something or get an approval/review during your work. It sends the
26
+ owner an offline notification; tapping it lands them in the conversation and
27
+ auto-starts a voice-brain call.
28
+
29
+ - `session_id` (required) — the session to call the owner into.
30
+
31
+ ## Rules
32
+
33
+ 1. `grix_session_send` only works when the owner is a member of that session;
34
+ otherwise it fails on scope — surface the error, don't retry blindly.
35
+ 2. `grix_call_owner` requires the owner to have configured a voice brain and is
36
+ rate-limited per session. Use it only when you genuinely need the owner, not
37
+ as a routine notification.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: grix-query
3
+ description: Use the typed `grix_query` tool for Grix contact lookup, keyword search, session search, and session message history lookup. Trigger when users ask to find contacts, search conversations, list visible sessions, or inspect recent messages in a known session.
4
+ trigger: 当用户要查找联系人、搜索会话、列出可见会话、或查看某个已知会话的历史消息时
5
+ ---
6
+
7
+ # Grix Query
8
+
9
+ Use the `grix_query` tool for read-only Grix lookup. This skill only queries
10
+ existing contacts, sessions, and raw session messages — it never sends or
11
+ changes anything.
12
+
13
+ ## Tool contract
14
+
15
+ Always call the `grix_query` tool with one `action`:
16
+
17
+ - `contact_search` — find contacts. Use exactly one mode: exact lookup with
18
+ `id`, keyword search with `keyword`, or list-all with neither.
19
+ - `session_search` — find sessions. Same three modes as `contact_search`.
20
+ - `message_history` — read recent messages in a session. Requires `sessionId`;
21
+ page backwards with `beforeId`.
22
+ - `message_search` — keyword search inside a session. Requires `sessionId` and
23
+ `keyword`.
24
+
25
+ Other parameters: `limit` (1–100), `offset`.
26
+
27
+ ## Rules
28
+
29
+ 1. Parse the request into exactly one action before calling.
30
+ 2. If both `id` and `keyword` are given, the backend prioritizes `id`; do not
31
+ send both unless you explicitly want exact-match behavior.
32
+ 3. For message history or in-session search when no `sessionId` is known, first
33
+ locate the session via `session_search`, or ask the user for a precise target.
34
+ 4. When a result is paginated and `has_more` is true, keep paging only when the
35
+ user asked for everything, the target is still unresolved, or one page is
36
+ clearly insufficient.
37
+ 5. On scope/auth/parameter errors, report the exact failure and the fix; do not
38
+ silently retry with guessed parameters.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: grix-task-status
3
+ description: Observe state — query the task state of all the owner's sessions (`grix_task_query`), and check the current MCP connection status (`grix_status`). Trigger when the user asks which tasks are running/done/waiting, or to verify the Grix connection is healthy.
4
+ trigger: 当用户问哪些任务在跑/已完成/在等待审批,或要确认 Grix 连接是否正常时
5
+ ---
6
+
7
+ # Grix Task & Status
8
+
9
+ Read-only observation of task and connection state.
10
+
11
+ ## Owner task states — `grix_task_query`
12
+
13
+ Query the session-level task state across all the owner's sessions. Takes no
14
+ parameters — owner and agent are resolved from the authenticated connection.
15
+ Returns one entry per session with a single mutually-exclusive state:
16
+
17
+ - `running` — working
18
+ - `waiting_approval` — blocked on the owner to approve/deny
19
+ - `waiting_question` — asked the owner a question, awaiting reply
20
+ - `completed` / `failed` — finished
21
+ - `idle` — no task / stopped
22
+
23
+ Use it to see at a glance which tasks are done, still running, or waiting on the
24
+ owner.
25
+
26
+ ## Connection status — `grix_status`
27
+
28
+ Query the Grix connection status of the current MCP session. Takes no
29
+ parameters. Use it to confirm the agent is connected before relying on other
30
+ grix tools.
31
+
32
+ ## Rules
33
+
34
+ 1. Both tools are read-only — safe to call any time to orient yourself.
35
+ 2. `grix_task_query` reports per-session state, not per-message; pair it with
36
+ `grix_query` (`message_history`) when you need the actual content.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: message-send
3
+ description: Send a message into a specific Grix session by session ID, including cross-session and proactive sends. For replying to the current event use the reply/complete tools instead. Trigger words: send DM, DM, send message, notify, message another session.
4
+ trigger: 当用户要主动给某个指定会话发消息、跨会话发送、或通知另一个会话时
5
+ ---
6
+
7
+ # Message Send
8
+
9
+ Use the `grix_message_send` tool to deliver a message into a specific Grix
10
+ session identified by its `sessionId`.
11
+
12
+ ## When to use which tool
13
+
14
+ - **Replying to the current event** (the one you are handling right now): use
15
+ the `reply` / `grix_reply` tool and finish with `complete`. Do NOT use
16
+ `grix_message_send` for this — it is for other sessions.
17
+ - **Sending to another session, or proactively starting one**: use
18
+ `grix_message_send` with that session's `sessionId`.
19
+
20
+ ## Tool contract
21
+
22
+ Call `grix_message_send`:
23
+
24
+ - `sessionId` (required) — the exact target session ID.
25
+ - `content` (required) — the message text (max 10000 chars).
26
+ - `msgType` (optional) — message type, default 1 (text).
27
+ - `quotedMessageId` (optional) — message ID to quote/reply to.
28
+ - `threadId` (optional) — thread ID for a threaded reply.
29
+
30
+ ## Rules
31
+
32
+ 1. You must have an exact `sessionId`. If you only have a name or keyword,
33
+ resolve it first with `grix_query` (`session_search` / `contact_search`).
34
+ 2. Never guess a `sessionId`. If it cannot be resolved, ask the user.
35
+ 3. Sending as yourself (the agent). To speak on the owner's behalf in a session
36
+ the owner belongs to, use the `grix-owner-relay` skill (`grix_session_send`).
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: message-unsend
3
+ description: Silently recall/unsend an already-sent message in a Grix session. After execution, end immediately without replying any confirmation text. Trigger words: recall, unsend, delete message, withdraw message.
4
+ trigger: 当用户要撤回、收回、删除一条已经发出的消息时
5
+ ---
6
+
7
+ # Message Unsend
8
+
9
+ Use the `grix_message_unsend` tool to recall a message that was already sent.
10
+
11
+ ## Tool contract
12
+
13
+ Call `grix_message_unsend`:
14
+
15
+ - `sessionId` (required) — the session the message lives in.
16
+ - `msgId` (required) — the ID of the message to recall.
17
+
18
+ ## Rules
19
+
20
+ 1. You need both the `sessionId` and the exact `msgId`. If the `msgId` is
21
+ unknown, find it first with `grix_query` (`message_history` /
22
+ `message_search`).
23
+ 2. This is a silent operation: after a successful recall, end immediately — do
24
+ not send any confirmation message back to the chat.
25
+ 3. Only recall messages that were actually sent; recalling someone else's
26
+ message will fail on scope/permission — surface that error rather than
27
+ retrying.
package/dist/grix.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import g from"node:path";import{writeFileSync as x}from"node:fs";import{Manager as $}from"./manager.js";import{ensureGrixDirs as L,initLogger as N,log as a,installProcessLogRotation as b,setConsoleOutput as C}from"./core/log/index.js";import{HealthServer as U}from"./core/runtime/index.js";import{writePidFile as H,removePidFile as h}from"./core/runtime/index.js";import{resolveRuntimePaths as v}from"./core/config/index.js";import{ServiceManager as j}from"./service/service-manager.js";import{killProcessesByCommandLine as _,isWindowsElevated as G}from"./service/process-control.js";import{acquireDaemonLock as W,releaseDaemonLock as w}from"./runtime/daemon-lock.js";import{writeDaemonStatus as k,removeDaemonStatus as M}from"./runtime/service-state.js";import{AdminServer as B,generateToken as V,writeTokenFile as X}from"./core/admin/index.js";import{initSentry as q,closeSentry as P,reportFatal as E}from"./core/observability/sentry.js";const c=process.argv.slice(2),A=[],s={};for(let t=0;t<c.length;t++)c[t].startsWith("--")&&c[t+1]&&!c[t+1].startsWith("--")?(s[c[t].slice(2)]=c[t+1],t++):c[t].startsWith("--")?s[c[t].slice(2)]="true":A.push(c[t]);s.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
2
+ import g from"node:path";import{writeFileSync as x}from"node:fs";import{Manager as $}from"./manager.js";import{ensureGrixDirs as L,initLogger as N,log as n,installProcessLogRotation as b,setConsoleOutput as C}from"./core/log/index.js";import{HealthServer as U}from"./core/runtime/index.js";import{writePidFile as H,removePidFile as h}from"./core/runtime/index.js";import{resolveRuntimePaths as v}from"./core/config/index.js";import{ServiceManager as j}from"./service/service-manager.js";import{killProcessesByCommandLine as _,isWindowsElevated as G}from"./service/process-control.js";import{acquireDaemonLock as W,releaseDaemonLock as w}from"./runtime/daemon-lock.js";import{writeDaemonStatus as k,removeDaemonStatus as M}from"./runtime/service-state.js";import{AdminServer as B,generateToken as V,writeTokenFile as X}from"./core/admin/index.js";import{initSentry as q,closeSentry as P,reportFatal as E}from"./core/observability/sentry.js";const c=process.argv.slice(2),A=[],s={};for(let t=0;t<c.length;t++)c[t].startsWith("--")&&c[t+1]&&!c[t+1].startsWith("--")?(s[c[t].slice(2)]=c[t+1],t++):c[t].startsWith("--")?s[c[t].slice(2)]="true":A.push(c[t]);s.help&&(console.log(`grix-connector \u2014 Unified AI Agent Bridge
3
3
 
4
4
  Usage: grix-connector <command> [options]
5
5
 
@@ -28,4 +28,4 @@ Examples:
28
28
  `),process.exit(0));const d=A[0],I=["start","stop","restart","status"];if(d&&I.includes(d)){process.platform==="win32"&&["start","stop","restart"].includes(d)&&!G()&&console.warn(`Warning: Not running as administrator. Task Scheduler registration is skipped;
29
29
  using Startup folder auto-start instead. For full Task Scheduler integration,
30
30
  right-click the terminal and select "Run as administrator".`);const t=v(),m=s["config-dir"]??(s.profile?g.join(t.configDir,s.profile):void 0),l=g.resolve(process.argv[1]||`${t.rootDir}/dist/grix.js`),r=new j({cliPath:l,nodePath:process.execPath});try{let o;switch(d){case"start":(await r.status({rootDir:t.rootDir})).installed?o=await r.start({rootDir:t.rootDir}):o=await r.install({rootDir:t.rootDir,configDir:m});break;case"stop":o=await r.stop({rootDir:t.rootDir});break;case"restart":(await r.status({rootDir:t.rootDir})).installed?o=await r.restart({rootDir:t.rootDir}):o=await r.install({rootDir:t.rootDir,configDir:m});break;case"status":o=await r.status({rootDir:t.rootDir});break}console.log(JSON.stringify(o,null,2)),process.exit(0)}catch(o){console.error(`${d} failed: ${o instanceof Error?o.message:o}`),process.exit(1)}}else d&&(console.error(`Unknown command: ${d}
31
- Valid commands: ${I.join(", ")}`),process.exit(1));const i=v(),J=s["config-dir"]??(s.profile?`${i.configDir}/${s.profile}`:void 0),n=new $,S=new U,R=V(),p=new B(R);let T=!1;async function D(t){if(T)return;T=!0,a.info("main",`Received ${t}, shutting down...`),S.markShuttingDown();const m=setTimeout(()=>{a.error("main","Shutdown timed out, forcing exit"),w(i.daemonLockFile).catch(()=>{}),h(),process.exit(2)},1e4);try{await n.stop(),await p.stop(),await S.stop(),await P(),await w(i.daemonLockFile),await M(i.daemonStatusFile).catch(()=>{}),clearTimeout(m),h(),a.info("main","Shutdown complete"),process.exit(0)}catch(l){a.error("main",`Shutdown error: ${l}`),w(i.daemonLockFile).catch(()=>{}),h(),process.exit(2)}}async function K(){L(),N(),q(),b(i.stdoutLogFile,i.stderrLogFile),C(!1),process.platform==="win32"&&await _("GrixConnectorDaemon",{platform:"win32"});try{await W(i.daemonLockFile,i.rootDir)}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}H(),a.info("main",`grix-connector starting (PID ${process.pid})`),await k(i.daemonStatusFile,{state:"starting",pid:process.pid,updated_at:Date.now()});const t=parseInt(s["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);await S.start(t);const m=g.join(i.dataDir,"health-port");x(m,String(t),"utf-8"),process.on("SIGINT",()=>D("SIGINT")),process.on("SIGTERM",()=>D("SIGTERM"));let l="",r=0,o;process.on("uncaughtException",e=>{const u=e instanceof Error?e.stack??e.message:String(e);u===l?(r++,(r<=3||r%100===0)&&a.error("main",`Uncaught exception (x${r}): ${u}`)):(r>3&&a.error("main",`Previous exception repeated ${r} times total`),l=u,r=1,a.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),o||(o=setTimeout(()=>{r>3&&a.error("main",`Previous exception repeated ${r} times total`),l="",r=0,o=void 0},1e4).unref())),!F(e)&&(E(e,"uncaughtException"),D("uncaughtException"))}),process.on("unhandledRejection",e=>{a.error("main",`Unhandled rejection: ${e}`),!F(e)&&(E(e,"unhandledRejection"),D("unhandledRejection"))}),S.setStatusProvider(()=>n.getAgentsStatus()),await n.start(J);const f=parseInt(s["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);p.setAgentHandler({list:()=>n.getAgentsStatus(),add:e=>n.addAgent(e),remove:e=>n.removeAgent(e),restart:e=>n.restartAgent(e)}),p.setUpgradeHandler({check:()=>n.checkUpgrade(),trigger:()=>n.triggerUpgrade()}),p.setProbeHandler({probeAll:e=>n.probeAll(e),probeOne:(e,u)=>n.probeOne(e,u)}),p.setInstallHandler({listInstallable:()=>n.listInstallable(),installAgent:e=>n.installAgent(e),getInstallProgress:e=>n.getInstallProgress(e)}),await p.start(f);const y=g.join(i.dataDir,"admin-token"),O=g.join(i.dataDir,"admin-port");X(y,R),x(O,String(f),"utf-8"),await k(i.daemonStatusFile,{state:"running",pid:process.pid,updated_at:Date.now()}),process.send&&process.send("ready"),a.info("main","grix-connector ready")}K().catch(async t=>{a.error("main",`Fatal: ${t}`),E(t,"startup"),await P(),w(i.daemonLockFile).catch(()=>{}),h(),process.exit(1)});const z=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function F(t){return t instanceof Error&&"code"in t?z.has(t.code):!1}
31
+ Valid commands: ${I.join(", ")}`),process.exit(1));const i=v(),J=s["config-dir"]??(s.profile?`${i.configDir}/${s.profile}`:void 0),a=new $,S=new U,R=V(),p=new B(R);let T=!1;async function D(t){if(T)return;T=!0,n.info("main",`Received ${t}, shutting down...`),S.markShuttingDown();const m=setTimeout(()=>{n.error("main","Shutdown timed out, forcing exit"),w(i.daemonLockFile).catch(()=>{}),h(),process.exit(2)},1e4);try{await a.stop(),await p.stop(),await S.stop(),await P(),await w(i.daemonLockFile),await M(i.daemonStatusFile).catch(()=>{}),clearTimeout(m),h(),n.info("main","Shutdown complete"),process.exit(0)}catch(l){n.error("main",`Shutdown error: ${l}`),w(i.daemonLockFile).catch(()=>{}),h(),process.exit(2)}}async function K(){L(),N(),await q(),b(i.stdoutLogFile,i.stderrLogFile),C(!1),process.platform==="win32"&&await _("GrixConnectorDaemon",{platform:"win32"});try{await W(i.daemonLockFile,i.rootDir)}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}H(),n.info("main",`grix-connector starting (PID ${process.pid})`),await k(i.daemonStatusFile,{state:"starting",pid:process.pid,updated_at:Date.now()});const t=parseInt(s["health-port"]??process.env.GRIX_HEALTH_PORT??"19579",10);await S.start(t);const m=g.join(i.dataDir,"health-port");x(m,String(t),"utf-8"),process.on("SIGINT",()=>D("SIGINT")),process.on("SIGTERM",()=>D("SIGTERM"));let l="",r=0,o;process.on("uncaughtException",e=>{const u=e instanceof Error?e.stack??e.message:String(e);u===l?(r++,(r<=3||r%100===0)&&n.error("main",`Uncaught exception (x${r}): ${u}`)):(r>3&&n.error("main",`Previous exception repeated ${r} times total`),l=u,r=1,n.error("main",`Uncaught exception: ${e instanceof Error?e.stack:e}`),o||(o=setTimeout(()=>{r>3&&n.error("main",`Previous exception repeated ${r} times total`),l="",r=0,o=void 0},1e4).unref())),!F(e)&&(E(e,"uncaughtException"),D("uncaughtException"))}),process.on("unhandledRejection",e=>{n.error("main",`Unhandled rejection: ${e}`),!F(e)&&(E(e,"unhandledRejection"),D("unhandledRejection"))}),S.setStatusProvider(()=>a.getAgentsStatus()),await a.start(J);const f=parseInt(s["admin-port"]??process.env.GRIX_ADMIN_PORT??"19580",10);p.setAgentHandler({list:()=>a.getAgentsStatus(),add:e=>a.addAgent(e),remove:e=>a.removeAgent(e),restart:e=>a.restartAgent(e)}),p.setUpgradeHandler({check:()=>a.checkUpgrade(),trigger:()=>a.triggerUpgrade()}),p.setProbeHandler({probeAll:e=>a.probeAll(e),probeOne:(e,u)=>a.probeOne(e,u)}),p.setInstallHandler({listInstallable:()=>a.listInstallable(),installAgent:e=>a.installAgent(e),getInstallProgress:e=>a.getInstallProgress(e)}),await p.start(f);const y=g.join(i.dataDir,"admin-token"),O=g.join(i.dataDir,"admin-port");X(y,R),x(O,String(f),"utf-8"),await k(i.daemonStatusFile,{state:"running",pid:process.pid,updated_at:Date.now()}),process.send&&process.send("ready"),n.info("main","grix-connector ready")}K().catch(async t=>{n.error("main",`Fatal: ${t}`),E(t,"startup"),await P(),w(i.daemonLockFile).catch(()=>{}),h(),process.exit(1)});const z=new Set(["ECONNRESET","ECONNREFUSED","ETIMEDOUT","EPIPE","EAI_AGAIN","ENOTFOUND","EHOSTUNREACH","ENETUNREACH","EIO"]);function F(t){return t instanceof Error&&"code"in t?z.has(t.code):!1}
package/dist/manager.js CHANGED
@@ -1,2 +1,2 @@
1
- import{readFileSync as Q,readdirSync as F,writeFileSync as B}from"node:fs";import{join as A}from"node:path";import{AgentInstance as S}from"./bridge/bridge.js";import{GRIX_PATHS as x,log as c}from"./core/log/index.js";import{resolveClientVersion as L}from"./core/util/client-version.js";import{UpgradeChecker as R}from"./core/upgrade/upgrade-checker.js";import{AgentGlobalConfigStore as z}from"./core/persistence/agent-global-config-store.js";import{scanSkills as G,dedupeSkills as K}from"./adapter/claude/skill-scanner.js";import{scanDefaultSkills as W,logDefaultSkillsCheck as V,cleanupProjectedSkills as D}from"./default-skills/index.js";import{resolveCopilotCommand as q}from"./core/runtime/copilot-resolve.js";import{getCliVersion as J,resolveCliPath as X}from"./core/util/cli-probe.js";import{AgentInstaller as Y}from"./core/installer/installer.js";import{reportInstallFailure as Z}from"./core/observability/sentry.js";const ee=8e3;function te(){const n=q();return[{clientType:"openclaw",command:"openclaw"},{clientType:"claude",command:"claude"},{clientType:"codex",command:"codex"},{clientType:"gemini",command:"gemini"},{clientType:"qwen",command:"qwen"},{clientType:"hermes",command:"hermes"},{clientType:"reasonix",command:"reasonix"},{clientType:"codewhale",command:"codewhale"},{clientType:"opencode",command:"opencode"},{clientType:"pi",command:"pi"},{clientType:"kiro",command:"kiro-cli"},{clientType:"copilot",command:n.command},{clientType:"agy",command:"agy"}]}async function ne(){return Promise.all(te().map(async n=>{const e=await X(n.command);if(!e)return{client_type:n.clientType,command:n.command,installed:!1,path:null,version:null};const t=await J(e,n.versionArgs??["--version"]);return{client_type:n.clientType,command:n.command,installed:!0,path:e,version:t.version,error:t.error}}))}function ae(n){switch(n){case"claude":return{adapterType:"claude",command:"claude"};case"codex":return{adapterType:"codex",command:"codex",options:{sandboxMode:"danger-full-access"}};case"gemini":return{adapterType:"acp",command:"gemini",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"qwen":return{adapterType:"acp",command:"qwen",adapterHint:"qwen/base",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"pi":return{adapterType:"pi",command:"pi"};case"cursor":return{adapterType:"cursor",command:"agent"};case"reasonix":return{adapterType:"acp",command:"reasonix",args:["acp"],enableSessionBinding:!0};case"codewhale":return{adapterType:"codewhale",command:"codewhale",enableSessionBinding:!0};case"openhuman":return{adapterType:"openhuman",command:"openhuman-core",enableSessionBinding:!0};case"kiro":return{adapterType:"acp",command:"kiro-cli",args:["acp"],enableSessionBinding:!0};case"opencode":return{adapterType:"opencode",command:"opencode",args:["serve"],enableSessionBinding:!0};case"copilot":{const e=q();return{adapterType:"acp",command:e.command,args:[...e.prefixArgs,"--acp"],enableSessionBinding:!0}}case"agy":return{adapterType:"agy",command:"agy",enableSessionBinding:!0};case"hermes":throw new Error('client_type "hermes" is not handled by grix-connector. Hermes runs as a separate project \u2014 see https://github.com/askie/grix-hermes-python');default:throw new Error(`Unsupported client_type: ${n}`)}}function oe(n){const e=String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default";return A(x.data,`session-bindings-${e}.json`)}function ie(n){const e=String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default";return A(x.data,`active-events-${e}.json`)}function re(...n){const e=[],t=new Set;for(const o of n)for(const a of o??[]){const r=String(a??"").trim(),u=r.toLowerCase();!r||t.has(u)||(t.add(u),e.push(r))}return e.length>0?e:void 0}function se(n,e){const t={claude:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codex:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},cursor:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},acp:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},pi:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codewhale:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},openhuman:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},opencode:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},agy:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0}},o=t[n]??t.acp;return{maxConcurrent:e?.max_concurrent??o.maxConcurrent??1,maxQueued:e?.max_queued??o.maxQueued??3,queueTimeoutMs:e?.queue_timeout_ms??o.queueTimeoutMs??0,cancelableQueued:!0,cancelableRunning:!0}}function E(n){const e=L(),t=String(n.client_type??"").trim().toLowerCase(),o=ae(t),a=String(n.ws_url??"").trim(),r="get_session_usage",u="get_rate_limits",l="get_agent_global_config";if(!n.name?.trim())throw new Error("agent name is required");if(!a)throw new Error(`agent ${n.name}: ws_url is required`);if(!n.agent_id?.trim())throw new Error(`agent ${n.name}: agent_id is required`);if(!n.api_key?.trim())throw new Error(`agent ${n.name}: api_key is required`);const s=o.adapterType,d=s==="acp",f=t==="qwen",p={...o.options??{}},i=s==="codex"?{capabilities:["local_action_v1","agent_invoke"],localActions:["session_control","get_context","set_model","set_mode","set_reasoning_effort","set_sandbox_mode","exec_approve","exec_reject","file_list","create_folder","turn_interrupt","permission_approve","permission_reject","thread_compact",r,u]}:null,m=s==="claude"?{localActions:["session_control","set_mode","set_model","claude_interaction_reply","exec_approve","exec_reject","file_list","create_folder","thread_compact",r,u]}:null,_=f?{capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",r],adapterHint:"qwen/base"}:null,h=s==="pi"?{adapterHint:"pi/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",r]}:null,g=s==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",r]}:null,w=s==="cursor"?{adapterHint:"cursor/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","set_mode","get_context","file_list","create_folder",r,u]}:null,b=s==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",r]}:null,T=s==="opencode"?{adapterHint:"opencode/base",capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",r]}:null,k=s==="agy"?{adapterHint:"agy/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",r]}:null,P=d&&!f?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",r]}:null,H=t==="kiro"?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder","thread_compact",r,u]}:null,N=s==="codex"||s==="claude"||t==="gemini"?["session_control","set_model","set_mode"]:void 0,O=[r,u];d&&p.raw_transport===void 0&&(p.raw_transport=t==="gemini");const U=`${t}/base`,$=s==="claude"?"claude":s==="codex"?"codex":s==="pi"?"pi":t==="kiro"?"kiro":"gemini";let y;try{const j=$==="kiro"?void 0:process.cwd();y=G({mode:$,projectDir:j})??void 0,y&&y.length===0&&(y=void 0)}catch{}const M=W();if(M.length>0){const v=K([...y??[],...M]),I=v.filter(C=>C.source==="connector").map(C=>C.name);I.length>0&&c.info("manager",`[${n.name}] injecting connector skills: [${I.join(", ")}]`),y=v.length>0?v:void 0}return{name:n.name,adapterType:s,aibot:{url:a,agentId:n.agent_id,apiKey:n.api_key,clientType:t,clientVersion:e,adapterHint:o.adapterHint??_?.adapterHint??h?.adapterHint??g?.adapterHint??w?.adapterHint??T?.adapterHint??k?.adapterHint??U,capabilities:i?.capabilities??b?.capabilities??h?.capabilities??g?.capabilities??w?.capabilities??T?.capabilities??k?.capabilities??_?.capabilities??["stream_chunk","local_action_v1","connector_upgrade"],localActions:re(i?.localActions??b?.localActions??m?.localActions??h?.localActions??g?.localActions??w?.localActions??T?.localActions??k?.localActions??_?.localActions??H?.localActions??P?.localActions??["exec_approve","exec_reject"],N,O,["connector_rollback","connector_upgrade_push",l]),skills:y},agent:{command:o.command,args:o.args,env:void 0},adapterOptions:p,acpAuthMethod:p.auth_method,acpInitialMode:p.initial_mode,acpMcpTools:p.acp_mcp_tools,promptTimeoutMs:n.prompt_timeout_ms,bindingsPath:oe(n.name),activeEventStorePath:ie(n.name),...o.enableSessionBinding||d?{enableSessionBinding:!0}:{},...o.autoInjectArgs?{autoInjectArgs:o.autoInjectArgs}:{},poolMaxSize:n.pool?.maxSize,poolIdleTimeoutMs:n.pool?.idleTimeoutMs,eventQueue:se(s,n.event_queue),logDir:x.log,providerBaseUrl:n.provider_base_url?.trim()||void 0,providerApiKey:n.provider_api_key?.trim()||void 0}}function ce(){const n=process.env.GRIX_AGENT_STARTUP_WAIT_MS,e=Number(n);return Number.isFinite(e)&&e>=500?Math.floor(e):ee}class ke{instances=[];configMap=new Map;upgradeChecker=null;globalConfigStore;configDir=x.config;installer=new Y;async start(e){const t=e??x.config;this.configDir=t,c.info("manager",`Loading configs from ${t}`),V(),D(),this.globalConfigStore=new z(A(x.data,"agent-global-configs.json")),this.globalConfigStore.load();const o=F(t).filter(l=>l.endsWith(".json")).sort();if(o.length===0)throw new Error(`No config files found in ${t}`);const a=[];let r=0;for(const l of o)try{const s=Q(A(t,l),"utf-8"),d=JSON.parse(s);if(Array.isArray(d.agents)){if(d.agents.length===0){c.error("manager",`No agents array found in ${l}`),r++;continue}for(const f of d.agents)try{const p=E(f);a.push({config:p,file:l}),c.info("manager",`Loaded ${p.name} (${p.adapterType??"acp"}) from ${l}`)}catch(p){const i=typeof f?.name=="string"?f.name:"<unknown>";c.error("manager",`Invalid agent config in ${l} (name=${i}): ${p}`),r++}}else c.error("manager",`Unrecognized config format in ${l}`)}catch(s){c.error("manager",`Failed to load ${l}: ${s}`)}let u=0;if(a.length>0){const l=ce();c.info("manager",`Starting ${a.length} agent(s), startup wait=${l}ms`);const s=()=>this.upgradeChecker?.triggerCheck(),d=i=>{this.instances=this.instances.filter(m=>m!==i)},f=a.map(({config:i})=>{const m=new S(i,this.globalConfigStore);return m.setUpgradeTrigger(s),this.instances.push(m),this.configMap.set(i.name,i),{config:i,instance:m,startPromise:m.start()}}),p=await Promise.all(f.map(async i=>{const m=await new Promise(_=>{let h=!1;const g=setTimeout(()=>{h||(h=!0,_({kind:"timeout"}))},l);i.startPromise.then(()=>{h||(h=!0,clearTimeout(g),_({kind:"started"}))}).catch(w=>{h||(h=!0,clearTimeout(g),_({kind:"failed",error:w}))})});return{task:i,outcome:m}}));for(const{task:i,outcome:m}of p)if(m.kind!=="started"){if(m.kind==="failed"){d(i.instance),c.error("manager",`Failed to start ${i.config.name}: ${m.error}`);continue}u++,c.warn("manager",`Startup pending for ${i.config.name}, continue retrying in background`),i.startPromise.then(()=>{c.info("manager",`Delayed start succeeded: ${i.config.name}`)}).catch(_=>{d(i.instance),c.error("manager",`Delayed start failed: ${i.config.name}: ${_}`)})}if(this.instances.length>0){const i=Math.max(0,this.instances.length-u);c.info("manager",`${i}/${a.length} agent(s) running now`)}u>0&&c.warn("manager",`${u} agent(s) still connecting in background`)}if(this.instances.length===0&&a.length>0)throw new Error("All agent configurations failed to start");if(a.length>0){const l=a[0].config;this.upgradeChecker=new R([{apiKey:l.aibot.apiKey,wsUrl:l.aibot.url}],()=>this.instances.some(s=>s.getStatus().busy)),await this.upgradeChecker.start()}}async stop(){c.info("manager","Stopping all agents..."),this.upgradeChecker?.stop(),await Promise.allSettled(this.instances.map(e=>e.stop())),await this.globalConfigStore?.flush(),this.instances=[],D(),c.info("manager","All stopped")}getAgentsStatus(){return this.instances.map(e=>e.getStatus())}async addAgent(e){const t=E(e);if(this.instances.some(a=>a.name===t.name))throw new Error(`Agent "${t.name}" already exists`);const o=new S(t,this.globalConfigStore);o.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),await o.start(),this.instances.push(o),this.configMap.set(t.name,t),this.persistAgentsConfig(),c.info("manager",`Added agent: ${t.name}`)}async removeAgent(e){const t=this.instances.findIndex(a=>a.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const o=this.instances[t];this.instances.splice(t,1),this.configMap.delete(e),await o.stop(),this.persistAgentsConfig(),c.info("manager",`Removed agent: ${e}`)}persistAgentsConfig(){const e=A(this.configDir,"agents.json");try{const t=[];for(const[,a]of this.configMap)t.push({name:a.name,ws_url:a.aibot.url,agent_id:a.aibot.agentId,api_key:a.aibot.apiKey,client_type:a.aibot.clientType});B(e,JSON.stringify({agents:t},null,4)+`
1
+ import{readFileSync as Q,readdirSync as F,writeFileSync as B}from"node:fs";import{join as A}from"node:path";import{AgentInstance as S}from"./bridge/bridge.js";import{GRIX_PATHS as x,log as c}from"./core/log/index.js";import{resolveClientVersion as L}from"./core/util/client-version.js";import{UpgradeChecker as R}from"./core/upgrade/upgrade-checker.js";import{AgentGlobalConfigStore as z}from"./core/persistence/agent-global-config-store.js";import{scanSkills as G,dedupeSkills as K}from"./adapter/claude/skill-scanner.js";import{scanDefaultSkills as W,logDefaultSkillsCheck as V,cleanupProjectedSkills as D}from"./default-skills/index.js";import{resolveCopilotCommand as q}from"./core/runtime/copilot-resolve.js";import{getCliVersion as J,resolveCliPath as X}from"./core/util/cli-probe.js";import{AgentInstaller as Y}from"./core/installer/installer.js";import{reportInstallFailure as Z}from"./core/observability/sentry.js";const ee=8e3;function te(){const n=q();return[{clientType:"openclaw",command:"openclaw"},{clientType:"claude",command:"claude"},{clientType:"codex",command:"codex"},{clientType:"gemini",command:"gemini"},{clientType:"qwen",command:"qwen"},{clientType:"hermes",command:"hermes"},{clientType:"reasonix",command:"reasonix"},{clientType:"codewhale",command:"codewhale"},{clientType:"opencode",command:"opencode"},{clientType:"cursor",command:"agent"},{clientType:"pi",command:"pi"},{clientType:"openhuman",command:"openhuman-core"},{clientType:"kiro",command:"kiro-cli"},{clientType:"copilot",command:n.command},{clientType:"agy",command:"agy"}]}async function ne(){return Promise.all(te().map(async n=>{const e=await X(n.command);if(!e)return{client_type:n.clientType,command:n.command,installed:!1,path:null,version:null};const t=await J(e,n.versionArgs??["--version"]);return{client_type:n.clientType,command:n.command,installed:!0,path:e,version:t.version,error:t.error}}))}function ae(n){switch(n){case"claude":return{adapterType:"claude",command:"claude"};case"codex":return{adapterType:"codex",command:"codex",options:{sandboxMode:"danger-full-access"}};case"gemini":return{adapterType:"acp",command:"gemini",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"qwen":return{adapterType:"acp",command:"qwen",adapterHint:"qwen/base",autoInjectArgs:{acp:!0},enableSessionBinding:!0};case"pi":return{adapterType:"pi",command:"pi"};case"cursor":return{adapterType:"cursor",command:"agent"};case"reasonix":return{adapterType:"acp",command:"reasonix",args:["acp"],enableSessionBinding:!0};case"codewhale":return{adapterType:"codewhale",command:"codewhale",enableSessionBinding:!0};case"openhuman":return{adapterType:"openhuman",command:"openhuman-core",enableSessionBinding:!0};case"kiro":return{adapterType:"acp",command:"kiro-cli",args:["acp"],enableSessionBinding:!0};case"opencode":return{adapterType:"opencode",command:"opencode",args:["serve"],enableSessionBinding:!0};case"copilot":{const e=q();return{adapterType:"acp",command:e.command,args:[...e.prefixArgs,"--acp"],enableSessionBinding:!0}}case"agy":return{adapterType:"agy",command:"agy",enableSessionBinding:!0};case"hermes":throw new Error('client_type "hermes" is not handled by grix-connector. Hermes runs as a separate project \u2014 see https://github.com/askie/grix-hermes-python');default:throw new Error(`Unsupported client_type: ${n}`)}}function oe(n){const e=String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default";return A(x.data,`session-bindings-${e}.json`)}function ie(n){const e=String(n??"").trim().toLowerCase().replace(/[^a-z0-9._-]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"default";return A(x.data,`active-events-${e}.json`)}function re(...n){const e=[],t=new Set;for(const o of n)for(const a of o??[]){const r=String(a??"").trim(),u=r.toLowerCase();!r||t.has(u)||(t.add(u),e.push(r))}return e.length>0?e:void 0}function se(n,e){const t={claude:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codex:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},cursor:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},acp:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},pi:{maxConcurrent:1,maxQueued:5,queueTimeoutMs:0},codewhale:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},openhuman:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},opencode:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0},agy:{maxConcurrent:1,maxQueued:3,queueTimeoutMs:0}},o=t[n]??t.acp;return{maxConcurrent:e?.max_concurrent??o.maxConcurrent??1,maxQueued:e?.max_queued??o.maxQueued??3,queueTimeoutMs:e?.queue_timeout_ms??o.queueTimeoutMs??0,cancelableQueued:!0,cancelableRunning:!0}}function E(n){const e=L(),t=String(n.client_type??"").trim().toLowerCase(),o=ae(t),a=String(n.ws_url??"").trim(),r="get_session_usage",u="get_rate_limits",l="get_agent_global_config";if(!n.name?.trim())throw new Error("agent name is required");if(!a)throw new Error(`agent ${n.name}: ws_url is required`);if(!n.agent_id?.trim())throw new Error(`agent ${n.name}: agent_id is required`);if(!n.api_key?.trim())throw new Error(`agent ${n.name}: api_key is required`);const s=o.adapterType,d=s==="acp",f=t==="qwen",p={...o.options??{}},i=s==="codex"?{capabilities:["local_action_v1","agent_invoke"],localActions:["session_control","get_context","set_model","set_mode","set_reasoning_effort","set_sandbox_mode","exec_approve","exec_reject","file_list","create_folder","turn_interrupt","permission_approve","permission_reject","thread_compact",r,u]}:null,m=s==="claude"?{localActions:["session_control","set_mode","set_model","claude_interaction_reply","exec_approve","exec_reject","file_list","create_folder","thread_compact",r,u]}:null,_=f?{capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",r],adapterHint:"qwen/base"}:null,h=s==="pi"?{adapterHint:"pi/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","get_context","file_list","create_folder",r]}:null,g=s==="openhuman"?{adapterHint:"openhuman/base",capabilities:["local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",r]}:null,w=s==="cursor"?{adapterHint:"cursor/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","set_mode","get_context","file_list","create_folder",r,u]}:null,b=s==="codewhale"?{capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",r]}:null,T=s==="opencode"?{adapterHint:"opencode/base",capabilities:["stream_chunk","local_action_v1"],localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",r]}:null,k=s==="agy"?{adapterHint:"agy/base",capabilities:["stream_chunk","local_action_v1"],localActions:["session_control","set_model","file_list","create_folder",r]}:null,P=d&&!f?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder",r]}:null,H=t==="kiro"?{localActions:["exec_approve","exec_reject","permission_approve","permission_reject","session_control","set_model","set_mode","file_list","create_folder","thread_compact",r,u]}:null,N=s==="codex"||s==="claude"||t==="gemini"?["session_control","set_model","set_mode"]:void 0,O=[r,u];d&&p.raw_transport===void 0&&(p.raw_transport=t==="gemini");const U=`${t}/base`,$=s==="claude"?"claude":s==="codex"?"codex":s==="pi"?"pi":t==="kiro"?"kiro":"gemini";let y;try{const j=$==="kiro"?void 0:process.cwd();y=G({mode:$,projectDir:j})??void 0,y&&y.length===0&&(y=void 0)}catch{}const M=W();if(M.length>0){const v=K([...y??[],...M]),I=v.filter(C=>C.source==="connector").map(C=>C.name);I.length>0&&c.info("manager",`[${n.name}] injecting connector skills: [${I.join(", ")}]`),y=v.length>0?v:void 0}return{name:n.name,adapterType:s,aibot:{url:a,agentId:n.agent_id,apiKey:n.api_key,clientType:t,clientVersion:e,adapterHint:o.adapterHint??_?.adapterHint??h?.adapterHint??g?.adapterHint??w?.adapterHint??T?.adapterHint??k?.adapterHint??U,capabilities:i?.capabilities??b?.capabilities??h?.capabilities??g?.capabilities??w?.capabilities??T?.capabilities??k?.capabilities??_?.capabilities??["stream_chunk","local_action_v1","connector_upgrade"],localActions:re(i?.localActions??b?.localActions??m?.localActions??h?.localActions??g?.localActions??w?.localActions??T?.localActions??k?.localActions??_?.localActions??H?.localActions??P?.localActions??["exec_approve","exec_reject"],N,O,["connector_rollback","connector_upgrade_push",l]),skills:y},agent:{command:o.command,args:o.args,env:void 0},adapterOptions:p,acpAuthMethod:p.auth_method,acpInitialMode:p.initial_mode,acpMcpTools:p.acp_mcp_tools,promptTimeoutMs:n.prompt_timeout_ms,bindingsPath:oe(n.name),activeEventStorePath:ie(n.name),...o.enableSessionBinding||d?{enableSessionBinding:!0}:{},...o.autoInjectArgs?{autoInjectArgs:o.autoInjectArgs}:{},poolMaxSize:n.pool?.maxSize,poolIdleTimeoutMs:n.pool?.idleTimeoutMs,eventQueue:se(s,n.event_queue),logDir:x.log,providerBaseUrl:n.provider_base_url?.trim()||void 0,providerApiKey:n.provider_api_key?.trim()||void 0}}function ce(){const n=process.env.GRIX_AGENT_STARTUP_WAIT_MS,e=Number(n);return Number.isFinite(e)&&e>=500?Math.floor(e):ee}class ke{instances=[];configMap=new Map;upgradeChecker=null;globalConfigStore;configDir=x.config;installer=new Y;async start(e){const t=e??x.config;this.configDir=t,c.info("manager",`Loading configs from ${t}`),V(),D(),this.globalConfigStore=new z(A(x.data,"agent-global-configs.json")),this.globalConfigStore.load();const o=F(t).filter(l=>l.endsWith(".json")).sort();if(o.length===0)throw new Error(`No config files found in ${t}`);const a=[];let r=0;for(const l of o)try{const s=Q(A(t,l),"utf-8"),d=JSON.parse(s);if(Array.isArray(d.agents)){if(d.agents.length===0){c.error("manager",`No agents array found in ${l}`),r++;continue}for(const f of d.agents)try{const p=E(f);a.push({config:p,file:l}),c.info("manager",`Loaded ${p.name} (${p.adapterType??"acp"}) from ${l}`)}catch(p){const i=typeof f?.name=="string"?f.name:"<unknown>";c.error("manager",`Invalid agent config in ${l} (name=${i}): ${p}`),r++}}else c.error("manager",`Unrecognized config format in ${l}`)}catch(s){c.error("manager",`Failed to load ${l}: ${s}`)}let u=0;if(a.length>0){const l=ce();c.info("manager",`Starting ${a.length} agent(s), startup wait=${l}ms`);const s=()=>this.upgradeChecker?.triggerCheck(),d=i=>{this.instances=this.instances.filter(m=>m!==i)},f=a.map(({config:i})=>{const m=new S(i,this.globalConfigStore);return m.setUpgradeTrigger(s),this.instances.push(m),this.configMap.set(i.name,i),{config:i,instance:m,startPromise:m.start()}}),p=await Promise.all(f.map(async i=>{const m=await new Promise(_=>{let h=!1;const g=setTimeout(()=>{h||(h=!0,_({kind:"timeout"}))},l);i.startPromise.then(()=>{h||(h=!0,clearTimeout(g),_({kind:"started"}))}).catch(w=>{h||(h=!0,clearTimeout(g),_({kind:"failed",error:w}))})});return{task:i,outcome:m}}));for(const{task:i,outcome:m}of p)if(m.kind!=="started"){if(m.kind==="failed"){d(i.instance),c.error("manager",`Failed to start ${i.config.name}: ${m.error}`);continue}u++,c.warn("manager",`Startup pending for ${i.config.name}, continue retrying in background`),i.startPromise.then(()=>{c.info("manager",`Delayed start succeeded: ${i.config.name}`)}).catch(_=>{d(i.instance),c.error("manager",`Delayed start failed: ${i.config.name}: ${_}`)})}if(this.instances.length>0){const i=Math.max(0,this.instances.length-u);c.info("manager",`${i}/${a.length} agent(s) running now`)}u>0&&c.warn("manager",`${u} agent(s) still connecting in background`)}if(this.instances.length===0&&a.length>0)throw new Error("All agent configurations failed to start");if(a.length>0){const l=a[0].config;this.upgradeChecker=new R([{apiKey:l.aibot.apiKey,wsUrl:l.aibot.url}],()=>this.instances.some(s=>s.getStatus().busy)),await this.upgradeChecker.start()}}async stop(){c.info("manager","Stopping all agents..."),this.upgradeChecker?.stop(),await Promise.allSettled(this.instances.map(e=>e.stop())),await this.globalConfigStore?.flush(),this.instances=[],D(),c.info("manager","All stopped")}getAgentsStatus(){return this.instances.map(e=>e.getStatus())}async addAgent(e){const t=E(e);if(this.instances.some(a=>a.name===t.name))throw new Error(`Agent "${t.name}" already exists`);const o=new S(t,this.globalConfigStore);o.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),await o.start(),this.instances.push(o),this.configMap.set(t.name,t),this.persistAgentsConfig(),c.info("manager",`Added agent: ${t.name}`)}async removeAgent(e){const t=this.instances.findIndex(a=>a.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const o=this.instances[t];this.instances.splice(t,1),this.configMap.delete(e),await o.stop(),this.persistAgentsConfig(),c.info("manager",`Removed agent: ${e}`)}persistAgentsConfig(){const e=A(this.configDir,"agents.json");try{const t=[];for(const[,a]of this.configMap)t.push({name:a.name,ws_url:a.aibot.url,agent_id:a.aibot.agentId,api_key:a.aibot.apiKey,client_type:a.aibot.clientType});B(e,JSON.stringify({agents:t},null,4)+`
2
2
  `,"utf-8")}catch(t){c.error("manager",`Failed to persist agents config: ${t}`)}}async restartAgent(e){const t=this.instances.findIndex(u=>u.name===e);if(t===-1)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});const o=this.configMap.get(e);if(!o)throw Object.assign(new Error(`Config for "${e}" not found`),{code:"NOT_FOUND"});await this.instances[t].stop();const r=new S(o,this.globalConfigStore);r.setUpgradeTrigger(()=>this.upgradeChecker?.triggerCheck()),await r.start(),this.instances[t]=r,c.info("manager",`Restarted agent: ${e}`)}async checkUpgrade(){return this.upgradeChecker?this.upgradeChecker.checkForUpdate():{available:!1}}triggerUpgrade(){this.upgradeChecker?.triggerCheck()}async probeAll(e={}){return le(this.instances,e)}async probeOne(e,t={}){return de(this.instances,e,t)}listInstallable(){return this.installer.listInstallable()}async installAgent(e){const t=await this.installer.install(e);return Z(t),t}getInstallProgress(e){return this.installer.getProgress(e)??null}}async function le(n,e){const t=e.concurrency??4,o=Date.now(),a=new Array(n.length);await new Promise(d=>{let f=0,p=0;const i=n.length;if(i===0){d();return}function m(g){const w=n[g];w.probe(e).then(b=>{a[g]=b,_()},b=>{a[g]={agent_name:w.name,client_type:"unknown",adapter_type:"acp",ok:!1,status:"error",probed_at:Date.now(),duration_ms:0,cached:!1,cli:{command:"",installed:!1,path:null,version:null,error:{code:"internal",message:b?.message??String(b)}},conversation:{attempted:!1,ok:!1,latency_ms:null},config:{model:null,base_url:null,source:{model:"unknown",base_url:"unknown"}},process:{started:!1,alive:!1,busy:!1}},_()})}function _(){p++,f<i?m(f++):p===i&&d()}const h=Math.min(t,i);for(let g=0;g<h;g++)m(f++)});const r=a.filter(d=>d.status==="healthy").length,u=a.filter(d=>d.status==="degraded").length,l=a.filter(d=>d.status==="unavailable").length,s=await ne();return{ok:r===a.length&&a.length>0,total:a.length,healthy:r,degraded:u,unavailable:l,installed_clients:s,agents:a,probed_at:o,duration_ms:Date.now()-o}}async function de(n,e,t){const o=n.find(a=>a.name===e);if(!o)throw Object.assign(new Error(`Agent "${e}" not found`),{code:"NOT_FOUND"});return o.probe(t)}export{ke as Manager,ne as probeInstalledClientCommands,le as probeInstances,de as probeOneInstance};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import y from"node:http";import i from"node:process";import{TOOLS as h,toolCallToInvoke as N}from"../core/mcp/tools.js";function O(){const n=i.argv.slice(2);for(let r=0;r<n.length;r++)if(n[r]==="--api-url"&&n[r+1])return n[r+1];const t=i.env.GRIX_CONNECTOR_INTERNAL_API;if(t)return t;i.stderr.write(`FATAL: --api-url <url> required (or set GRIX_CONNECTOR_INTERNAL_API)
3
- `),i.exit(1)}const v=O();function u(n,t){return{jsonrpc:"2.0",id:n,result:t}}function f(n,t,r){return{jsonrpc:"2.0",id:n,error:{code:t,message:r}}}async function S(n,t){const r=new URL("/api/invoke",v),o=JSON.stringify({action:n,params:t,timeout_ms:15e3});return new Promise((m,e)=>{const s=y.request(r,{method:"POST",headers:{"content-type":"application/json","content-length":Buffer.byteLength(o)}},l=>{const g=[];l.on("data",c=>g.push(c)),l.on("end",()=>{const c=Buffer.concat(g).toString("utf8");try{const d=JSON.parse(c);d.ok?m(d.data??null):e(new Error(d.error??"invoke failed"))}catch{e(new Error(`invalid response: ${c.slice(0,200)}`))}})});s.on("error",l=>e(l)),s.write(o),s.end()})}function w(n){return u(n.id??null,{protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"grix-connector-tools",version:"1.0.0"}})}function E(n){return u(n.id??null,{tools:h})}async function I(n){const t=n.params??{},r=String(t.name??""),o=t.arguments??{};if(!h.find(e=>e.name===r))return f(n.id??null,-32602,`Unknown tool: ${r}`);try{const e=N(r,o),s=await S(e.action,e.params);return u(n.id??null,{content:[{type:"text",text:JSON.stringify(s,null,2)}]})}catch(e){const s=e instanceof Error?e.message:String(e);return u(n.id??null,{content:[{type:"text",text:JSON.stringify({error:s})}],isError:!0})}}let p="";i.stdin.setEncoding("utf8"),i.stdin.on("data",n=>{p+=n;const t=p.split(`
4
- `);p=t.pop()??"";for(const r of t){const o=r.trim();o&&T(o)}}),i.stdin.on("end",()=>{i.exit(0)});async function T(n){let t;try{t=JSON.parse(n)}catch{a(f(null,-32700,"Parse error"));return}try{switch(t.method){case"initialize":a(w(t));return;case"notifications/initialized":return;case"tools/list":a(E(t));return;case"tools/call":a(await I(t));return;default:a(f(t.id??null,-32601,`Method not found: ${t.method}`))}}catch(r){const o=r instanceof Error?r.message:String(r);a(f(t.id??null,-32603,`Internal error: ${o}`))}}function a(n){n&&i.stdout.write(`${JSON.stringify(n)}
2
+ import N from"node:http";import i from"node:process";import{TOOLS as y,toolCallToInvoke as O}from"../core/mcp/tools.js";function v(){const n=i.argv.slice(2);for(let r=0;r<n.length;r++)if(n[r]==="--api-url"&&n[r+1])return n[r+1];const t=i.env.GRIX_CONNECTOR_INTERNAL_API;if(t)return t;i.stderr.write(`FATAL: --api-url <url> required (or set GRIX_CONNECTOR_INTERNAL_API)
3
+ `),i.exit(1)}const w=v();function u(n,t){return{jsonrpc:"2.0",id:n,result:t}}function f(n,t,r){return{jsonrpc:"2.0",id:n,error:{code:t,message:r}}}async function S(n,t){const r=new URL(w),o=new URL("/api/invoke",r);o.search=r.search;const d=JSON.stringify({action:n,params:t,timeout_ms:15e3});return new Promise((e,s)=>{const p=N.request(o,{method:"POST",headers:{"content-type":"application/json","content-length":Buffer.byteLength(d)}},c=>{const g=[];c.on("data",l=>g.push(l)),c.on("end",()=>{const l=Buffer.concat(g).toString("utf8");try{const m=JSON.parse(l);m.ok?e(m.data??null):s(new Error(m.error??"invoke failed"))}catch{s(new Error(`invalid response: ${l.slice(0,200)}`))}})});p.on("error",c=>s(c)),p.write(d),p.end()})}function E(n){return u(n.id??null,{protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"grix-connector-tools",version:"1.0.0"}})}function I(n){return u(n.id??null,{tools:y})}async function L(n){const t=n.params??{},r=String(t.name??""),o=t.arguments??{};if(!y.find(e=>e.name===r))return f(n.id??null,-32602,`Unknown tool: ${r}`);try{const e=O(r,o),s=await S(e.action,e.params);return u(n.id??null,{content:[{type:"text",text:JSON.stringify(s,null,2)}]})}catch(e){const s=e instanceof Error?e.message:String(e);return u(n.id??null,{content:[{type:"text",text:JSON.stringify({error:s})}],isError:!0})}}let h="";i.stdin.setEncoding("utf8"),i.stdin.on("data",n=>{h+=n;const t=h.split(`
4
+ `);h=t.pop()??"";for(const r of t){const o=r.trim();o&&R(o)}}),i.stdin.on("end",()=>{i.exit(0)});async function R(n){let t;try{t=JSON.parse(n)}catch{a(f(null,-32700,"Parse error"));return}try{switch(t.method){case"initialize":a(E(t));return;case"notifications/initialized":return;case"tools/list":a(I(t));return;case"tools/call":a(await L(t));return;default:a(f(t.id??null,-32601,`Method not found: ${t.method}`))}}catch(r){const o=r instanceof Error?r.message:String(r);a(f(t.id??null,-32603,`Internal error: ${o}`))}}function a(n){n&&i.stdout.write(`${JSON.stringify(n)}
5
5
  `)}
@@ -1 +1 @@
1
- function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
1
+ function a(e){const t=new Set([`http://127.0.0.1:${e.serverPort}`,`http://localhost:${e.serverPort}`,...e.allowedOrigins]),o=new Set([`127.0.0.1:${e.serverPort}`,`localhost:${e.serverPort}`,...e.allowedHosts]);return{validateRequest(s){const r=i(s,t);if(!r.ok)return r;const n=l(s,o);return n.ok?{ok:!0}:n}}}function i(e,t){const o=e.headers.origin;return o?t.has(o)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${o}`}:{ok:!0}}function l(e,t){const o=e.headers.host;return o?t.has(o)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${o}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
@@ -9114,7 +9114,7 @@ var TOOLS = [
9114
9114
  var EVENT_TOOLS = [
9115
9115
  {
9116
9116
  name: "grix_reply",
9117
- description: "Send a reply message to the specified session. Supports streaming in chunks; the frontend will automatically aggregate them into one complete message.",
9117
+ description: "Send a reply message to the specified session. Supports streaming in chunks; the frontend will automatically aggregate them into one complete message. Any content meant for the user \u2014 including your final conclusion at the end of a turn \u2014 MUST be sent through this tool; text written outside this tool is not delivered to the user.",
9118
9118
  inputSchema: {
9119
9119
  type: "object",
9120
9120
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,6 +28,8 @@
28
28
  "dev": "npm run build && node dist/grix.js",
29
29
  "test": "node --experimental-vm-modules node_modules/vitest/vitest.mjs run",
30
30
  "test:watch": "node --experimental-vm-modules node_modules/vitest/vitest.mjs",
31
+ "test:e2e": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e",
32
+ "test:e2e:smoke": "RUN_REAL_AGENT_E2E=1 node --experimental-vm-modules node_modules/vitest/vitest.mjs run tests/e2e/smoke.test.ts",
31
33
  "lint": "npm run check:isolation && tsc --noEmit",
32
34
  "start": "node dist/grix.js start",
33
35
  "stop": "node dist/grix.js stop",