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.
- package/dist/adapter/acp/acp-adapter.js +9 -9
- package/dist/adapter/agy/agy-adapter.js +7 -3
- package/dist/adapter/claude/claude-adapter.js +18 -18
- package/dist/adapter/claude/usage-parser.js +5 -4
- package/dist/adapter/pi/pi-adapter.js +5 -5
- package/dist/bridge/bridge.js +9 -9
- package/dist/core/admin/admin-server.js +1 -1
- package/dist/core/aibot/client.js +2 -2
- package/dist/core/installer/installer.js +7 -7
- package/dist/core/installer/preflight.js +4 -3
- package/dist/core/installer/registry.js +1 -1
- package/dist/core/mcp/tools.js +1 -1
- package/dist/core/observability/sentry.js +1 -1
- package/dist/core/persistence/session-binding-store.js +1 -1
- package/dist/core/upgrade/npm-upgrader.js +2 -2
- package/dist/default-skills/grix-access-control/SKILL.md +31 -0
- package/dist/default-skills/grix-admin/SKILL.md +35 -0
- package/dist/default-skills/grix-agent-dispatch/SKILL.md +35 -0
- package/dist/default-skills/grix-group/SKILL.md +35 -0
- package/dist/default-skills/grix-owner-relay/SKILL.md +37 -0
- package/dist/default-skills/grix-query/SKILL.md +38 -0
- package/dist/default-skills/grix-task-status/SKILL.md +36 -0
- package/dist/default-skills/message-send/SKILL.md +36 -0
- package/dist/default-skills/message-unsend/SKILL.md +27 -0
- package/dist/grix.js +2 -2
- package/dist/manager.js +1 -1
- package/dist/mcp/acp-mcp-server.js +3 -3
- package/dist/mcp/stream-http/security.js +1 -1
- package/openclaw-plugin/index.js +1 -1
- package/package.json +3 -1
|
@@ -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
|
|
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),
|
|
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
|
|
3
|
-
`),i.exit(1)}const v
|
|
4
|
-
`);
|
|
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(
|
|
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};
|
package/openclaw-plugin/index.js
CHANGED
|
@@ -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.
|
|
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",
|