grix-connector 3.1.14 → 3.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -241
- package/dist/adapter/claude/claude-bridge-server.js +1 -0
- package/dist/adapter/claude/claude-tools.js +1 -0
- package/dist/adapter/claude/claude-worker-client.js +1 -0
- package/dist/adapter/claude/mcp-http-launcher.js +2 -0
- package/dist/adapter/claude/result-timeout.js +1 -0
- package/dist/adapter/deepseek/deepseek-adapter.js +6 -0
- package/dist/adapter/deepseek/index.js +1 -0
- package/dist/adapter/qwen/index.js +1 -0
- package/dist/adapter/qwen/qwen-adapter.js +4 -0
- package/dist/aibot/client.js +1 -0
- package/dist/aibot/index.js +1 -0
- package/dist/aibot/types.js +0 -0
- package/dist/bridge/bridge.js +7 -7
- package/dist/core/file-ops/handler.js +1 -0
- package/dist/core/file-ops/list-files.js +1 -0
- package/dist/core/file-ops/types.js +0 -0
- package/dist/core/mcp/tools.js +1 -1
- package/dist/default-skills/grix-access-control/SKILL.md +31 -31
- package/dist/default-skills/grix-admin/SKILL.md +35 -35
- package/dist/default-skills/grix-agent-dispatch/SKILL.md +89 -89
- package/dist/default-skills/grix-chat-state/SKILL.md +56 -56
- package/dist/default-skills/grix-egg/SKILL.md +90 -90
- package/dist/default-skills/grix-group/SKILL.md +35 -35
- package/dist/default-skills/grix-owner-relay/SKILL.md +66 -66
- package/dist/default-skills/grix-query/SKILL.md +41 -38
- package/dist/default-skills/grix-task-status/SKILL.md +36 -0
- package/dist/default-skills/message-send/SKILL.md +36 -36
- package/dist/default-skills/message-unsend/SKILL.md +27 -27
- package/dist/default-skills/tailnet-file-share/SKILL.md +65 -65
- package/dist/grix.js +0 -0
- package/dist/log.js +3 -0
- package/dist/main.js +31 -0
- package/dist/mcp/acp-mcp-server.js +3 -3
- package/dist/mcp/stdio/server.js +6 -6
- package/dist/mcp/stream-http/config.js +1 -0
- package/dist/mcp/stream-http/connection-binding.js +1 -0
- package/dist/mcp/stream-http/event-tool-executor.js +1 -0
- package/dist/mcp/stream-http/gateway.js +1 -0
- package/dist/mcp/stream-http/index.js +1 -0
- package/dist/mcp/stream-http/security.js +1 -0
- package/dist/mcp/stream-http/session-manager.js +1 -0
- package/dist/mcp/stream-http/tool-executor.js +1 -0
- package/dist/mcp/stream-http/tool-registry.js +1 -0
- package/dist/mcp/stream-http/tool-schemas.js +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/manager.js +1 -0
- package/dist/transport/index.js +1 -0
- package/dist/transport/json-rpc.js +3 -0
- package/openclaw-plugin/index.js +44 -5
- package/openclaw-plugin/skills/grix-admin/SKILL.md +202 -202
- package/openclaw-plugin/skills/grix-admin/references/api-contract.md +210 -210
- package/openclaw-plugin/skills/grix-egg/SKILL.md +81 -81
- package/openclaw-plugin/skills/grix-egg/references/api-contract.md +40 -40
- package/openclaw-plugin/skills/grix-group/SKILL.md +164 -164
- package/openclaw-plugin/skills/grix-group/references/api-contract.md +97 -97
- package/openclaw-plugin/skills/grix-query/SKILL.md +251 -247
- package/openclaw-plugin/skills/grix-register/SKILL.md +86 -86
- package/openclaw-plugin/skills/grix-register/references/api-contract.md +76 -76
- package/openclaw-plugin/skills/grix-register/references/grix-concepts.md +26 -26
- package/openclaw-plugin/skills/grix-register/references/handoff-contract.md +24 -24
- package/openclaw-plugin/skills/grix-register/references/openclaw-setup.md +6 -6
- package/openclaw-plugin/skills/grix-register/references/user-replies.md +25 -25
- package/openclaw-plugin/skills/grix-update/SKILL.md +310 -310
- package/openclaw-plugin/skills/grix-update/references/cron-setup.md +56 -56
- package/openclaw-plugin/skills/grix-update/references/update-contract.md +149 -149
- package/openclaw-plugin/skills/message-send/SKILL.md +197 -197
- package/openclaw-plugin/skills/message-unsend/SKILL.md +186 -186
- package/openclaw-plugin/skills/message-unsend/flowchart.mermaid +27 -27
- package/openclaw-plugin/skills/openclaw-memory-setup/SKILL.md +282 -282
- package/openclaw-plugin/skills/openclaw-memory-setup/references/case-study-macpro.md +52 -52
- package/openclaw-plugin/skills/openclaw-memory-setup/references/host-readiness.md +147 -147
- package/openclaw.plugin.json +24 -24
- package/package.json +121 -121
- package/scripts/install-guardian.mjs +27 -27
- package/scripts/install-guardian.sh +25 -25
- package/scripts/upgrade-guardian.sh +104 -104
package/README.md
CHANGED
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
# grix-connector
|
|
2
|
-
|
|
3
|
-
A command-line daemon that connects your local AI coding agents to the [Grix](https://grix.im) platform.
|
|
4
|
-
|
|
5
|
-
## What is Grix?
|
|
6
|
-
|
|
7
|
-
Grix is an AI Agent scheduling platform. It lets you manage and interact with multiple AI coding agents through a unified chat interface. Register at [grix.im](https://grix.im) to get started.
|
|
8
|
-
|
|
9
|
-
## Get the Client
|
|
10
|
-
|
|
11
|
-
After installing grix-connector, download the Grix client from [grix.im](https://grix.im) to chat with your agents. Clients are available for iOS, Android, macOS, Windows, and Linux.
|
|
12
|
-
|
|
13
|
-
## Supported Agents
|
|
14
|
-
|
|
15
|
-
Set `client_type` in your config to one of the values below. Each `client_type` maps to a built-in adapter and CLI command — you only need the corresponding CLI installed locally.
|
|
16
|
-
|
|
17
|
-
| `client_type` | Agent | Adapter | Required CLI |
|
|
18
|
-
|---|---|---|---|
|
|
19
|
-
| `claude` | Claude Code (Anthropic) | claude | `claude` |
|
|
20
|
-
| `codex` | Codex (OpenAI) | codex | `codex` |
|
|
21
|
-
| `gemini` | Gemini (Google) | acp | `gemini` |
|
|
22
|
-
| `qwen` | Qwen (Alibaba) | acp | `qwen` |
|
|
23
|
-
| `copilot` | GitHub Copilot | acp | `copilot` or `gh` |
|
|
24
|
-
| `kiro` | Kiro | acp | `kiro-cli` |
|
|
25
|
-
| `reasonix` | Reasonix | acp | `reasonix` |
|
|
26
|
-
| `cursor` | Cursor Agent | cursor | `agent` |
|
|
27
|
-
| `codewhale` | CodeWhale | codewhale | `codewhale` |
|
|
28
|
-
| `opencode` | OpenCode | opencode | `opencode` |
|
|
29
|
-
| `pi` | Pi | pi | `pi` |
|
|
30
|
-
| `openhuman` | OpenHuman | openhuman | `openhuman-core` |
|
|
31
|
-
| `agy` | Agy (Antigravity) | agy | `agy` |
|
|
32
|
-
| `hermes` | [Hermes](https://github.com/askie/grix-hermes-python) | external | `hermes` |
|
|
33
|
-
|
|
34
|
-
The ACP adapter (Agent Client Protocol over JSON-RPC) backs Gemini, Qwen, Copilot, Kiro and Reasonix. Hermes is an external agent maintained in a separate project — see [grix-hermes-python](https://github.com/askie/grix-hermes-python) for setup.
|
|
35
|
-
|
|
36
|
-
You need to have the corresponding CLI tool installed locally before connecting an agent.
|
|
37
|
-
|
|
38
|
-
## Install
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install -g grix-connector
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Requires Node.js >= 18.
|
|
45
|
-
|
|
46
|
-
On Windows, `grix-connector` uses the built-in Task Scheduler with a hidden WScript launcher (no extra dependency required).
|
|
47
|
-
|
|
48
|
-
## Quick Start
|
|
49
|
-
|
|
50
|
-
### 1. Register a Grix account
|
|
51
|
-
|
|
52
|
-
Go to [grix.im](https://grix.im), sign up and get your API key.
|
|
53
|
-
|
|
54
|
-
### 2. Create agent config
|
|
55
|
-
|
|
56
|
-
Create `~/.grix/config/agents.json`. Choose the `ws_url` for your region — the two regions use different WebSocket domains:
|
|
57
|
-
|
|
58
|
-
- China mainland: `wss://grix.dhf.pub/v1/agent-api/ws`
|
|
59
|
-
- Global: `wss://ws.grix.im/v1/agent-api/ws`
|
|
60
|
-
|
|
61
|
-
China mainland example:
|
|
62
|
-
|
|
63
|
-
```json
|
|
64
|
-
{
|
|
65
|
-
"agents": [
|
|
66
|
-
{
|
|
67
|
-
"name": "my-agent",
|
|
68
|
-
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
69
|
-
"agent_id": "your-agent-id",
|
|
70
|
-
"api_key": "your-grix-api-key",
|
|
71
|
-
"client_type": "claude"
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Global example:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"agents": [
|
|
82
|
-
{
|
|
83
|
-
"name": "my-agent",
|
|
84
|
-
"ws_url": "wss://ws.grix.im/v1/agent-api/ws",
|
|
85
|
-
"agent_id": "your-agent-id",
|
|
86
|
-
"api_key": "your-grix-api-key",
|
|
87
|
-
"client_type": "claude"
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Change `client_type` to match the agent you want to connect (see table above). You can define multiple agents in one file, or use separate files under `~/.grix/config/`.
|
|
94
|
-
|
|
95
|
-
### Config Reference
|
|
96
|
-
|
|
97
|
-
Each agent entry uses one flat structure:
|
|
98
|
-
|
|
99
|
-
| Field | Required | Description |
|
|
100
|
-
|---|---|---|
|
|
101
|
-
| `name` | yes | Display name for this agent |
|
|
102
|
-
| `ws_url` | yes | WebSocket endpoint URL (region-specific). China mainland: `wss://grix.dhf.pub/v1/agent-api/ws`; Global: `wss://ws.grix.im/v1/agent-api/ws` |
|
|
103
|
-
| `agent_id` | yes | Agent ID from Grix platform |
|
|
104
|
-
| `api_key` | yes | API key for authentication |
|
|
105
|
-
| `client_type` | yes | See Supported Agents table above |
|
|
106
|
-
| `prompt_timeout_ms` | no | Prompt execution timeout (ms) |
|
|
107
|
-
| `pool.maxSize` | no | Max adapter pool size (default 20) |
|
|
108
|
-
| `pool.idleTimeoutMs` | no | Idle adapter eviction timeout (default 300000 = 5 min) |
|
|
109
|
-
|
|
110
|
-
Adapter command/args/options are built in and resolved from `client_type`. To connect a different agent, simply change `client_type` — no other config changes needed.
|
|
111
|
-
|
|
112
|
-
### Multi-agent Example
|
|
113
|
-
|
|
114
|
-
China mainland region (swap the `ws_url` domain to `ws.grix.im` for the global region):
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{
|
|
118
|
-
"agents": [
|
|
119
|
-
{
|
|
120
|
-
"name": "my-claude",
|
|
121
|
-
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
122
|
-
"agent_id": "your-agent-id",
|
|
123
|
-
"api_key": "your-grix-api-key",
|
|
124
|
-
"client_type": "claude"
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"name": "my-gemini",
|
|
128
|
-
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
129
|
-
"agent_id": "another-agent-id",
|
|
130
|
-
"api_key": "your-grix-api-key",
|
|
131
|
-
"client_type": "gemini"
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### 3. Start the daemon
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
grix-connector start
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
The daemon connects to Grix via WebSocket and starts routing chat messages to your agents.
|
|
144
|
-
|
|
145
|
-
### Commands
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
grix-connector start # Start as system service (auto-installs on first run)
|
|
149
|
-
grix-connector stop # Stop the service
|
|
150
|
-
grix-connector restart # Restart the service
|
|
151
|
-
grix-connector reload # Hot-reload agent configs without restarting the daemon
|
|
152
|
-
grix-connector status # Check service status
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Reloading config (`reload`)
|
|
156
|
-
|
|
157
|
-
`reload` applies changes to your config files **without restarting the daemon**. It re-reads `~/.grix/config/*.json`, diffs against what's currently running, and acts per agent:
|
|
158
|
-
|
|
159
|
-
| Change in config | What happens |
|
|
160
|
-
|---|---|
|
|
161
|
-
| Agent added | Started and connected |
|
|
162
|
-
| Agent removed | Stopped and disconnected |
|
|
163
|
-
| Agent config changed | That agent restarts (stop old, start new) |
|
|
164
|
-
| Agent unchanged | **Left running untouched** — its sessions are not interrupted |
|
|
165
|
-
|
|
166
|
-
**When to use it.** You run a multi-agent setup and want to add, remove, or re-key one agent — or tweak one agent's settings — without dropping the live sessions of the *other* agents. A full `restart` would reconnect every agent and interrupt all in-flight conversations; `reload` only touches the agents whose config actually changed.
|
|
167
|
-
|
|
168
|
-
**Safety.** If any config file fails to parse (e.g. you're mid-edit and the JSON is broken), or no valid agent config is found, the reload aborts and the running agents are left exactly as they are — nothing is torn down on a bad config.
|
|
169
|
-
|
|
170
|
-
Three equivalent ways to trigger it:
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
grix-connector reload # CLI (sends SIGHUP to the daemon)
|
|
174
|
-
kill -HUP "$(cat ~/.grix/grix-acp.pid)" # raw signal (Unix)
|
|
175
|
-
curl -XPOST http://127.0.0.1:19580/api/reload # Admin API — returns the per-agent result as JSON
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
The CLI/signal form is fire-and-forget; the per-agent result is written to the daemon log. The Admin API form returns the result (added/removed/restarted/unchanged/failed) synchronously.
|
|
179
|
-
|
|
180
|
-
### Ports
|
|
181
|
-
|
|
182
|
-
The daemon binds two local loopback ports (127.0.0.1 only):
|
|
183
|
-
|
|
184
|
-
| Purpose | Default | Override (env) | Override (CLI) |
|
|
185
|
-
|---|---|---|---|
|
|
186
|
-
| Health check (`/healthz`) | `19579` | `GRIX_HEALTH_PORT` | `--health-port <port>` |
|
|
187
|
-
| Admin API (used by the local CLI) | `19580` | `GRIX_ADMIN_PORT` | `--admin-port <port>` |
|
|
188
|
-
|
|
189
|
-
If a port is already in use, the daemon refuses to start and writes a clear message to `~/.grix/service/daemon.err.log` and the main log, and marks `~/.grix/daemon-status.json` as `state: "failed"` with a `reason` like `port_bind_in_use:health:19579`.
|
|
190
|
-
|
|
191
|
-
To pick different ports:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
# via environment
|
|
195
|
-
GRIX_HEALTH_PORT=29579 GRIX_ADMIN_PORT=29580 grix-connector restart
|
|
196
|
-
|
|
197
|
-
# or via CLI flags (when running the daemon directly)
|
|
198
|
-
grix-connector --health-port 29579 --admin-port 29580
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
To find what is occupying a port:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
# macOS / Linux
|
|
205
|
-
lsof -nP -iTCP:19579 -sTCP:LISTEN
|
|
206
|
-
|
|
207
|
-
# Windows (PowerShell or cmd)
|
|
208
|
-
netstat -ano | findstr :19579
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## OpenClaw Plugin
|
|
212
|
-
|
|
213
|
-
grix-connector can also be installed as an [OpenClaw](https://openclaw.io) plugin, providing a Grix channel transport with admin tools and operator CLI.
|
|
214
|
-
|
|
215
|
-
### Install
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
openclaw plugin install grix-connector
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Or manually add to your OpenClaw project:
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
npm install grix-connector
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Plugin Features
|
|
228
|
-
|
|
229
|
-
- **Channel**: Grix chat transport — routes messages between OpenClaw and your Grix deployment
|
|
230
|
-
- **Tools**: `grix_query`, `grix_group`, `grix_admin`, `grix_egg`, `grix_register`, `grix_update`, `grix_message_send`, `grix_message_unsend`, `openclaw_memory_setup`
|
|
231
|
-
- **CLI**: `openclaw grix` — agent management and admin commands
|
|
232
|
-
- **Skills**: 9 bundled skills for admin, group, query, registration, update, messaging, memory setup, and egg orchestration
|
|
233
|
-
|
|
234
|
-
### Requirements
|
|
235
|
-
|
|
236
|
-
- OpenClaw >= 2026.4.8
|
|
237
|
-
- A Grix account with agent ID and API key
|
|
238
|
-
|
|
239
|
-
## License
|
|
240
|
-
|
|
241
|
-
MIT
|
|
1
|
+
# grix-connector
|
|
2
|
+
|
|
3
|
+
A command-line daemon that connects your local AI coding agents to the [Grix](https://grix.im) platform.
|
|
4
|
+
|
|
5
|
+
## What is Grix?
|
|
6
|
+
|
|
7
|
+
Grix is an AI Agent scheduling platform. It lets you manage and interact with multiple AI coding agents through a unified chat interface. Register at [grix.im](https://grix.im) to get started.
|
|
8
|
+
|
|
9
|
+
## Get the Client
|
|
10
|
+
|
|
11
|
+
After installing grix-connector, download the Grix client from [grix.im](https://grix.im) to chat with your agents. Clients are available for iOS, Android, macOS, Windows, and Linux.
|
|
12
|
+
|
|
13
|
+
## Supported Agents
|
|
14
|
+
|
|
15
|
+
Set `client_type` in your config to one of the values below. Each `client_type` maps to a built-in adapter and CLI command — you only need the corresponding CLI installed locally.
|
|
16
|
+
|
|
17
|
+
| `client_type` | Agent | Adapter | Required CLI |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `claude` | Claude Code (Anthropic) | claude | `claude` |
|
|
20
|
+
| `codex` | Codex (OpenAI) | codex | `codex` |
|
|
21
|
+
| `gemini` | Gemini (Google) | acp | `gemini` |
|
|
22
|
+
| `qwen` | Qwen (Alibaba) | acp | `qwen` |
|
|
23
|
+
| `copilot` | GitHub Copilot | acp | `copilot` or `gh` |
|
|
24
|
+
| `kiro` | Kiro | acp | `kiro-cli` |
|
|
25
|
+
| `reasonix` | Reasonix | acp | `reasonix` |
|
|
26
|
+
| `cursor` | Cursor Agent | cursor | `agent` |
|
|
27
|
+
| `codewhale` | CodeWhale | codewhale | `codewhale` |
|
|
28
|
+
| `opencode` | OpenCode | opencode | `opencode` |
|
|
29
|
+
| `pi` | Pi | pi | `pi` |
|
|
30
|
+
| `openhuman` | OpenHuman | openhuman | `openhuman-core` |
|
|
31
|
+
| `agy` | Agy (Antigravity) | agy | `agy` |
|
|
32
|
+
| `hermes` | [Hermes](https://github.com/askie/grix-hermes-python) | external | `hermes` |
|
|
33
|
+
|
|
34
|
+
The ACP adapter (Agent Client Protocol over JSON-RPC) backs Gemini, Qwen, Copilot, Kiro and Reasonix. Hermes is an external agent maintained in a separate project — see [grix-hermes-python](https://github.com/askie/grix-hermes-python) for setup.
|
|
35
|
+
|
|
36
|
+
You need to have the corresponding CLI tool installed locally before connecting an agent.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g grix-connector
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Node.js >= 18.
|
|
45
|
+
|
|
46
|
+
On Windows, `grix-connector` uses the built-in Task Scheduler with a hidden WScript launcher (no extra dependency required).
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
### 1. Register a Grix account
|
|
51
|
+
|
|
52
|
+
Go to [grix.im](https://grix.im), sign up and get your API key.
|
|
53
|
+
|
|
54
|
+
### 2. Create agent config
|
|
55
|
+
|
|
56
|
+
Create `~/.grix/config/agents.json`. Choose the `ws_url` for your region — the two regions use different WebSocket domains:
|
|
57
|
+
|
|
58
|
+
- China mainland: `wss://grix.dhf.pub/v1/agent-api/ws`
|
|
59
|
+
- Global: `wss://ws.grix.im/v1/agent-api/ws`
|
|
60
|
+
|
|
61
|
+
China mainland example:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"agents": [
|
|
66
|
+
{
|
|
67
|
+
"name": "my-agent",
|
|
68
|
+
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
69
|
+
"agent_id": "your-agent-id",
|
|
70
|
+
"api_key": "your-grix-api-key",
|
|
71
|
+
"client_type": "claude"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Global example:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"agents": [
|
|
82
|
+
{
|
|
83
|
+
"name": "my-agent",
|
|
84
|
+
"ws_url": "wss://ws.grix.im/v1/agent-api/ws",
|
|
85
|
+
"agent_id": "your-agent-id",
|
|
86
|
+
"api_key": "your-grix-api-key",
|
|
87
|
+
"client_type": "claude"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Change `client_type` to match the agent you want to connect (see table above). You can define multiple agents in one file, or use separate files under `~/.grix/config/`.
|
|
94
|
+
|
|
95
|
+
### Config Reference
|
|
96
|
+
|
|
97
|
+
Each agent entry uses one flat structure:
|
|
98
|
+
|
|
99
|
+
| Field | Required | Description |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `name` | yes | Display name for this agent |
|
|
102
|
+
| `ws_url` | yes | WebSocket endpoint URL (region-specific). China mainland: `wss://grix.dhf.pub/v1/agent-api/ws`; Global: `wss://ws.grix.im/v1/agent-api/ws` |
|
|
103
|
+
| `agent_id` | yes | Agent ID from Grix platform |
|
|
104
|
+
| `api_key` | yes | API key for authentication |
|
|
105
|
+
| `client_type` | yes | See Supported Agents table above |
|
|
106
|
+
| `prompt_timeout_ms` | no | Prompt execution timeout (ms) |
|
|
107
|
+
| `pool.maxSize` | no | Max adapter pool size (default 20) |
|
|
108
|
+
| `pool.idleTimeoutMs` | no | Idle adapter eviction timeout (default 300000 = 5 min) |
|
|
109
|
+
|
|
110
|
+
Adapter command/args/options are built in and resolved from `client_type`. To connect a different agent, simply change `client_type` — no other config changes needed.
|
|
111
|
+
|
|
112
|
+
### Multi-agent Example
|
|
113
|
+
|
|
114
|
+
China mainland region (swap the `ws_url` domain to `ws.grix.im` for the global region):
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"agents": [
|
|
119
|
+
{
|
|
120
|
+
"name": "my-claude",
|
|
121
|
+
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
122
|
+
"agent_id": "your-agent-id",
|
|
123
|
+
"api_key": "your-grix-api-key",
|
|
124
|
+
"client_type": "claude"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "my-gemini",
|
|
128
|
+
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
129
|
+
"agent_id": "another-agent-id",
|
|
130
|
+
"api_key": "your-grix-api-key",
|
|
131
|
+
"client_type": "gemini"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. Start the daemon
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
grix-connector start
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The daemon connects to Grix via WebSocket and starts routing chat messages to your agents.
|
|
144
|
+
|
|
145
|
+
### Commands
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
grix-connector start # Start as system service (auto-installs on first run)
|
|
149
|
+
grix-connector stop # Stop the service
|
|
150
|
+
grix-connector restart # Restart the service
|
|
151
|
+
grix-connector reload # Hot-reload agent configs without restarting the daemon
|
|
152
|
+
grix-connector status # Check service status
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Reloading config (`reload`)
|
|
156
|
+
|
|
157
|
+
`reload` applies changes to your config files **without restarting the daemon**. It re-reads `~/.grix/config/*.json`, diffs against what's currently running, and acts per agent:
|
|
158
|
+
|
|
159
|
+
| Change in config | What happens |
|
|
160
|
+
|---|---|
|
|
161
|
+
| Agent added | Started and connected |
|
|
162
|
+
| Agent removed | Stopped and disconnected |
|
|
163
|
+
| Agent config changed | That agent restarts (stop old, start new) |
|
|
164
|
+
| Agent unchanged | **Left running untouched** — its sessions are not interrupted |
|
|
165
|
+
|
|
166
|
+
**When to use it.** You run a multi-agent setup and want to add, remove, or re-key one agent — or tweak one agent's settings — without dropping the live sessions of the *other* agents. A full `restart` would reconnect every agent and interrupt all in-flight conversations; `reload` only touches the agents whose config actually changed.
|
|
167
|
+
|
|
168
|
+
**Safety.** If any config file fails to parse (e.g. you're mid-edit and the JSON is broken), or no valid agent config is found, the reload aborts and the running agents are left exactly as they are — nothing is torn down on a bad config.
|
|
169
|
+
|
|
170
|
+
Three equivalent ways to trigger it:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
grix-connector reload # CLI (sends SIGHUP to the daemon)
|
|
174
|
+
kill -HUP "$(cat ~/.grix/grix-acp.pid)" # raw signal (Unix)
|
|
175
|
+
curl -XPOST http://127.0.0.1:19580/api/reload # Admin API — returns the per-agent result as JSON
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The CLI/signal form is fire-and-forget; the per-agent result is written to the daemon log. The Admin API form returns the result (added/removed/restarted/unchanged/failed) synchronously.
|
|
179
|
+
|
|
180
|
+
### Ports
|
|
181
|
+
|
|
182
|
+
The daemon binds two local loopback ports (127.0.0.1 only):
|
|
183
|
+
|
|
184
|
+
| Purpose | Default | Override (env) | Override (CLI) |
|
|
185
|
+
|---|---|---|---|
|
|
186
|
+
| Health check (`/healthz`) | `19579` | `GRIX_HEALTH_PORT` | `--health-port <port>` |
|
|
187
|
+
| Admin API (used by the local CLI) | `19580` | `GRIX_ADMIN_PORT` | `--admin-port <port>` |
|
|
188
|
+
|
|
189
|
+
If a port is already in use, the daemon refuses to start and writes a clear message to `~/.grix/service/daemon.err.log` and the main log, and marks `~/.grix/daemon-status.json` as `state: "failed"` with a `reason` like `port_bind_in_use:health:19579`.
|
|
190
|
+
|
|
191
|
+
To pick different ports:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# via environment
|
|
195
|
+
GRIX_HEALTH_PORT=29579 GRIX_ADMIN_PORT=29580 grix-connector restart
|
|
196
|
+
|
|
197
|
+
# or via CLI flags (when running the daemon directly)
|
|
198
|
+
grix-connector --health-port 29579 --admin-port 29580
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
To find what is occupying a port:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# macOS / Linux
|
|
205
|
+
lsof -nP -iTCP:19579 -sTCP:LISTEN
|
|
206
|
+
|
|
207
|
+
# Windows (PowerShell or cmd)
|
|
208
|
+
netstat -ano | findstr :19579
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## OpenClaw Plugin
|
|
212
|
+
|
|
213
|
+
grix-connector can also be installed as an [OpenClaw](https://openclaw.io) plugin, providing a Grix channel transport with admin tools and operator CLI.
|
|
214
|
+
|
|
215
|
+
### Install
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
openclaw plugin install grix-connector
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Or manually add to your OpenClaw project:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
npm install grix-connector
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Plugin Features
|
|
228
|
+
|
|
229
|
+
- **Channel**: Grix chat transport — routes messages between OpenClaw and your Grix deployment
|
|
230
|
+
- **Tools**: `grix_query`, `grix_group`, `grix_admin`, `grix_egg`, `grix_register`, `grix_update`, `grix_message_send`, `grix_message_unsend`, `openclaw_memory_setup`
|
|
231
|
+
- **CLI**: `openclaw grix` — agent management and admin commands
|
|
232
|
+
- **Skills**: 9 bundled skills for admin, group, query, registration, update, messaging, memory setup, and egg orchestration
|
|
233
|
+
|
|
234
|
+
### Requirements
|
|
235
|
+
|
|
236
|
+
- OpenClaw >= 2026.4.8
|
|
237
|
+
- A Grix account with agent ID and API key
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const r of t)e.push(r);const n=Buffer.concat(e).toString("utf8").trim();return n?JSON.parse(n):{}}function k(t){const e=(t.headers.authorization??"").trim();return e.toLowerCase().startsWith("bearer ")?e.slice(7).trim():""}class w{host="127.0.0.1";port=0;token;callbacks;server=null;address=null;constructor(e){this.token=d(),this.callbacks=e}getToken(){return this.token}getURL(){return this.address?`http://${this.address.address}:${this.address.port}`:""}async start(){this.server||(this.server=c.createServer(async(e,n)=>{try{await this.handleRequest(e,n)}catch(r){h(n,r instanceof Error?r.message:String(r))}}),await new Promise((e,n)=>{this.server.once("error",n),this.server.listen(this.port,this.host,()=>{this.server.off("error",n),e()})}),this.address=this.server.address(),o.info("claude-bridge",`Bridge server listening on ${this.getURL()}`))}async stop(){if(!this.server)return;const e=this.server;this.server=null,this.address=null,e.closeIdleConnections?.(),e.closeAllConnections?.(),await new Promise((n,r)=>{e.close(s=>s?r(s):n())})}async handleRequest(e,n){if(k(e)!==this.token){l(n);return}if(e.method!=="POST"){n.writeHead(405,{"content-type":"application/json"}),n.end(JSON.stringify({error:"method_not_allowed"}));return}const r=new URL(e.url,"http://localhost").pathname,s=await v(e),i=f.get(r);if(!i){u(n);return}const a=await i(this.callbacks,s);p(n,a??{ok:!0})}}const f=new Map([["/v1/worker/register",async(t,e)=>(o.info("claude-bridge",`Worker registered: ${e.worker_id} (pid=${e.pid})`),t.onRegisterWorker(e))],["/v1/worker/status",async(t,e)=>(o.info("claude-bridge",`Worker status: ${e.status}`),t.onStatusUpdate(e))],["/v1/worker/send-text",async(t,e)=>t.onSendText(e)],["/v1/worker/send-stream-chunk",async(t,e)=>t.onSendStreamChunk(e)],["/v1/worker/send-media",async(t,e)=>t.onSendMedia(e)],["/v1/worker/delete-message",async(t,e)=>t.onDeleteMessage(e)],["/v1/worker/ack-event",async(t,e)=>t.onAckEvent(e)],["/v1/worker/event-result",async(t,e)=>t.onSendEventResult(e)],["/v1/worker/event-stop-ack",async(t,e)=>t.onSendEventStopAck(e)],["/v1/worker/event-stop-result",async(t,e)=>t.onSendEventStopResult(e)],["/v1/worker/session-composing",async(t,e)=>t.onSetSessionComposing(e)],["/v1/worker/agent-invoke",async(t,e)=>t.onAgentInvoke(e)],["/v1/worker/local-action-result",async(t,e)=>t.onLocalActionResult(e)]]);export{w as ClaudeBridgeServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as x}from"node:crypto";import{CallToolRequestSchema as S,ListToolsRequestSchema as w}from"@modelcontextprotocol/sdk/types.js";import{log as f}from"../../core/log/index.js";import{toolCallToInvoke as k}from"../../core/mcp/tools.js";const E=new Set(["contact_search","session_search","message_history","message_search","group_create","group_detail_read","group_leave_self","group_member_add","group_member_remove","group_member_role_update","group_all_members_muted_update","group_member_speaking_update","group_dissolve","send_msg","delete_msg","agent_api_create","agent_category_list","agent_category_create","agent_category_update","agent_category_assign","agent_api_key_rotate"]),y=3e4,I=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},files:{type:"array",items:{type:"string"},description:"Optional absolute local file paths. Each file is uploaded through Agent API OSS presign before sending."},final:{type:"boolean",description:"Whether this is the final reply for the event. Defaults to false \u2014 the event stays open while Claude continues working, and auto-completes after inactivity. Set true only when this is definitively the last message for the event."}},required:["chat_id","event_id"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},code:{type:"string"},msg:{type:"string"}},required:["event_id","status"]}},{name:"delete_message",description:"Delete a previously sent message in the same grix-claude chat.",inputSchema:{type:"object",properties:{chat_id:{type:"string"},message_id:{type:"string"}},required:["chat_id","message_id"]}},{name:"status",description:"Show grix-claude runtime status, upstream access state, bridge health, and startup hints.",inputSchema:{type:"object",properties:{}}},{name:"send",description:"Send a message to a chat session proactively, without requiring an inbound event. Use for notifications or scheduled reports.",inputSchema:{type:"object",properties:{chat_id:{type:"string",description:"The target chat/session id."},text:{type:"string",description:"The message text to send."}},required:["chat_id","text"]}},{name:"access_pair",description:"Forward a sender pairing approval code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"access_deny",description:"Forward a sender pairing denial code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"allow_sender",description:"Ask upstream access control to allow a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"remove_sender",description:"Ask upstream access control to remove a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"access_policy",description:"Ask upstream access control to update the sender access policy.",inputSchema:{type:"object",properties:{policy:{type:"string",enum:["allowlist","open","disabled"]}},required:["policy"]}},{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},keyword:{type:"string"},id:{type:"string"},sessionId:{type:"string"},limit:{type:"number"},offset:{type:"number"},beforeId:{type:"string"}},required:["action"]}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string"},memberIds:{type:"array",items:{type:"string"}},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer",description:"Member type (for update_member_role / update_member_speaking)."},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted (for update_member_speaking)."}},required:["action"]}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},content:{type:"string"},msgType:{type:"number"},quotedMessageId:{type:"string"},threadId:{type:"string"}},required:["sessionId","content"]}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},msgId:{type:"string"}},required:["sessionId","msgId"]}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentId:{type:"string"},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"number"}},required:["action"]}}];function q(r,e){r.setRequestHandler(w,async()=>({tools:I})),r.setRequestHandler(S,async i=>{const{name:n,arguments:t}=i.params,s=t??{};try{switch(n){case"reply":return await A(s,e);case"complete":return await $(s,e);case"delete_message":return await T(s,e);case"status":return j(e);case"send":return await M(s,e);case"access_pair":case"access_deny":case"allow_sender":case"remove_sender":case"access_policy":return await R(n,s,e);case"grix_query":case"grix_group":case"grix_message_send":case"grix_message_unsend":case"grix_admin":return await O(n,s,e);default:return{content:[{type:"text",text:`Unknown tool: ${n}`}],isError:!0}}}catch(a){return f.error("claude-tools",`Tool ${n} error: ${a}`),{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}})}async function A(r,e){const i=e.getActiveEvent();if(!i)return{content:[{type:"text",text:"No active event to reply to"}],isError:!0};const n=String(r.text??""),t=String(r.chat_id??""),s=String(r.event_id??i.eventId),a=r.reply_to,d=r.files,_=r.final===!0;if(!t||!s)return{content:[{type:"text",text:"reply requires chat_id and event_id"}],isError:!0};if(!n.trim()&&(!d||d.length===0))return{content:[{type:"text",text:"reply requires at least one of text or files"}],isError:!0};const{text:g,quotedMessageId:v}=e.resolveQuotedMessageId(a,n),b=[];let m=0;const h=`reply_${s}_${Date.now()}`;try{if(g){const p=e.splitText(g);for(let o=0;o<p.length;o++){if(!e.isEventActive(s))return c("ignored: event no longer active");m++,e.bridge.sendStreamChunk(s,t,p[o],++i.chunkSeq,!1,h)}}if(d&&d.length>0)for(const p of d){if(!e.isEventActive(s))return c("ignored: event no longer active");m++;const o=await e.uploadFile(p,t),l=`${x()}_${m}`;e.bridge.sendMedia(s,t,o.access_url,o.file_name,v,l,o.extra),f.info("claude-tools",`File sent: ${o.file_name}`)}e.bridge.sendStreamChunk(s,t,"",++i.chunkSeq,!0,h)}catch(p){if(g&&b.length===0)try{const o=`fallback_${s}_${Date.now()}`,l=e.splitText(g);for(let u=0;u<l.length;u++)e.bridge.sendStreamChunk("",t,l[u],u+1,!1,o);return e.bridge.sendStreamChunk("",t,"",l.length+1,!0,o),e.markReplySent(s),_&&e.finalizeEvent(s,"responded"),c("sent via fallback")}catch{}if(!e.isEventActive(s))return c("ignored: event no longer active");throw e.bridge.sendEventResult(s,"failed",String(p),"send_msg_failed"),p}return e.markReplySent(s),_?e.finalizeEvent(s,"responded"):(i.responded=!0,e.clearActiveEvent("completed")),c("Reply sent")}async function $(r,e){const i=e.getActiveEvent(),n=String(r.event_id??""),t=r.status??"",s=r.code,a=r.msg;if(!n||!t)return{content:[{type:"text",text:"complete requires event_id and status"}],isError:!0};const d=["responded","canceled","failed"];return d.includes(t)?e.isEventActive(n)?(e.bridge.sendEventResult(n,t,a,s),e.clearActiveEvent(t),c("Event completed")):c("ignored: event no longer active"):{content:[{type:"text",text:`status must be one of: ${d.join(", ")}`}],isError:!0}}async function T(r,e){const i=String(r.chat_id??""),n=String(r.message_id??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and message_id are required"}],isError:!0};try{return await e.bridge.agentInvoke("grix_message_unsend",{sessionId:i,msgId:n},y),c(`deleted (${n})`)}catch(t){return{content:[{type:"text",text:`Delete failed: ${t}`}],isError:!0}}}function j(r){const e=r.getStatusInfo();return{content:[{type:"text",text:JSON.stringify({alive:e.alive,active_event:e.activeEvent,pending_approvals:e.pendingPermissions,pending_questions:e.pendingElicitations})}]}}async function M(r,e){const i=String(r.chat_id??""),n=String(r.text??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and text are required"}],isError:!0};try{const t=e.splitText(n),s=`send_${i}_${Date.now()}`;for(let a=0;a<t.length;a++)e.bridge.sendStreamChunk("",i,t[a],a+1,!1,s);return e.bridge.sendStreamChunk("",i,"",t.length+1,!0,s),c("sent")}catch(t){return{content:[{type:"text",text:`Send failed: ${t}`}],isError:!0}}}const C={access_pair:{verb:"pair_approve",payloadKey:"code"},access_deny:{verb:"pair_deny",payloadKey:"code"},allow_sender:{verb:"sender_allow",payloadKey:"sender_id"},remove_sender:{verb:"sender_remove",payloadKey:"sender_id"},access_policy:{verb:"policy_set",payloadKey:"policy"}};async function R(r,e,i){try{const n=C[r];if(!n)throw new Error(`Unknown access control tool: ${r}`);const t={};e.code!=null&&(t.code=e.code),e.sender_id!=null&&(t.sender_id=e.sender_id),e.policy!=null&&(t.policy=e.policy);const s=await i.bridge.agentInvoke("claude_access_control",{verb:n.verb,payload:t},y);return{content:[{type:"text",text:typeof s=="string"?s:JSON.stringify(s)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}async function O(r,e,i){try{const n=k(r,e);if(!E.has(n.action))throw new Error(`Action not allowed: ${n.action}`);const t=await i.bridge.agentInvoke(n.action,n.params,y);if(t&&Number(t.code??0)!==0)throw new Error(String(t.msg??"invoke failed"));return{content:[{type:"text",text:t?.data!=null?typeof t.data=="string"?t.data:JSON.stringify(t.data):JSON.stringify(t)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}function c(r){return{content:[{type:"text",text:r}]}}export{q as registerClaudeTools};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as l}from"../../core/log/index.js";class c{controlURL="";token="";isConfigured(){return!!(this.controlURL&&this.token)}configure(r,e){this.controlURL=r.replace(/\/+$/,""),this.token=e.trim(),l.info("claude-worker-client",`Configured with control URL: ${this.controlURL}`)}async post(r,e,s){if(!this.isConfigured())throw new Error("worker control not configured");const i=new AbortController,o=setTimeout(()=>i.abort(),s);try{const t=await fetch(`${this.controlURL}${r}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.token}`},body:JSON.stringify(e),signal:i.signal}),n=await t.text(),a=n.trim()?JSON.parse(n):{};if(!t.ok)throw new Error(a.error||`worker control failed ${t.status}`);return a}finally{clearTimeout(o)}}isRetryableError(r){const e=r instanceof Error?r.message:String(r);return/fetch failed|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|aborted/i.test(e)}async postWithRetry(r,e,s,i=1){let o;for(let t=0;t<=i;t++)try{return t>0&&l.info("claude-worker-client",`Retrying ${r} attempt=${t+1}`),await this.post(r,e,s)}catch(n){if(o=n,t>=i||!this.isRetryableError(n))break;await new Promise(a=>setTimeout(a,150))}throw o instanceof Error?o:new Error(String(o))}async deliverEvent(r){return this.postWithRetry("/v1/worker/deliver-event",{payload:r},1e4,1)}async deliverStop(r){return this.postWithRetry("/v1/worker/deliver-stop",{payload:r},1e4,1)}async deliverLocalAction(r){return this.postWithRetry("/v1/worker/deliver-local-action",{payload:r},1e4,1)}async ping(){try{return await this.postWithRetry("/v1/worker/ping",{},5e3,1),!0}catch{return!1}}}export{c as ClaudeWorkerClient};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{spawn as x,execSync as y}from"node:child_process";import{randomUUID as v}from"node:crypto";import{mkdir as S}from"node:fs/promises";import{readFileSync as C}from"node:fs";import{join as d}from"node:path";import{homedir as T,tmpdir as I}from"node:os";import{log as o}from"../../core/log/index.js";import{MCP_HTTP_CHANNEL_NAME as m}from"./protocol-contract.js";function P(t){let e=null,r=0,n=!1,i=!1;const a=v(),s=t.gatewayUrl??"http://127.0.0.1:19580/mcp";return{async start(){await F(t.command,s,t.env);const c=E(t.grix),u=[...t.args??[],"--name",`grix-mcp-${t.name}`,"--session-id",a];t.fullAuto&&u.push("--dangerously-skip-permissions"),u.push("--dangerously-load-development-channels",`server:${m}`,"--append-system-prompt",c);const f=d(I(),`grix-mcp-claude-${t.name}`);await S(f,{recursive:!0});const{expectPath:$,pidPath:g}=await M(f,t.command,u),_={...process.env,...t.env??{}};e=x("/usr/bin/expect",[$],{cwd:t.cwd,env:_,stdio:["ignore","pipe","pipe"],detached:!0}),o.info("mcp-http-launcher",`\u542F\u52A8 Claude: name=${t.name} cwd=${t.cwd} pid=${e.pid}`),r=await k(g),n=!0,o.info("mcp-http-launcher",`Claude \u5B50\u8FDB\u7A0B PID: ${r}`),e.on("exit",(l,p)=>{o.info("mcp-http-launcher",`Claude \u9000\u51FA: code=${l} signal=${p}`),n=!1,e=null,r=0,i||(o.info("mcp-http-launcher","3 \u79D2\u540E\u81EA\u52A8\u91CD\u542F..."),setTimeout(()=>{i||this.start().catch(w=>{o.error("mcp-http-launcher",`\u91CD\u542F\u5931\u8D25: ${w}`)})},3e3))}),e.stdout?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stdout] ${p.slice(0,300)}`)}),e.stderr?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stderr] ${p.slice(0,300)}`)})},async stop(){if(i=!0,n=!1,r>0)try{process.kill(r,"SIGTERM")}catch{}if(e?.pid){try{process.kill(-e.pid,"SIGTERM")}catch{}await new Promise(c=>{const u=setTimeout(()=>{if(r>0)try{process.kill(r,"SIGKILL")}catch{}if(e?.pid)try{process.kill(-e.pid,"SIGKILL")}catch{}c()},5e3);e?.once("exit",()=>{clearTimeout(u),c()})})}e=null,r=0},getStatus(){return{name:t.name,alive:n,pid:r}}}}function E(t){return["You are connected to a chat via the grix MCP server.",`On startup, immediately call grix_authorize with: agentId="${t.agentId}", apiKey="${t.apiKey}", wsUrl="${t.wsUrl}", clientType="${t.clientType}".`,"When you receive a <channel> message, you MUST respond by calling the grix_reply tool (or the grix_complete tool if no response is needed).","Never write your reply as plain text \u2014 it will NOT reach the user. Only the grix_reply tool delivers your response to the chat.","The <channel> message contains event_id and session_id \u2014 pass them to grix_reply."].join(" ")}async function F(t,e,r){const n=d(T(),".claude.json");let i=null;try{const s=C(n,"utf8");i=JSON.parse(s)?.mcpServers?.[m]??null}catch{}if(i&&String(i.type??"").trim()==="http"&&String(i.url??"").trim()===e)return;o.info("mcp-http-launcher",`\u6CE8\u518C MCP Server: ${m} -> ${e}`);const a={...process.env,...r??{}};try{y(`${t} mcp remove -s user ${m}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}catch{}y(`${t} mcp add --scope user --transport http ${m} ${e}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}async function M(t,e,r){const{writeFile:n}=await import("node:fs/promises"),i=d(t,"claude.pid"),a=d(t,"claude.expect"),s=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set claude_command [list {${h(e)}}${r.map(c=>` {${h(c)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${h(i)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)I am using this for local development} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {Listening for channel} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," -re {bypass permissions} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," eof {}","}","expect eof",""];return await n(i,"","utf8"),await n(a,s.join(`
|
|
2
|
+
`),"utf8"),{expectPath:a,pidPath:i}}function h(t){return t.replace(/[\\{}$\[\]"]/g,"\\$&")}async function k(t,e=1e4){const{readFile:r}=await import("node:fs/promises"),n=Math.ceil(e/100);for(let i=0;i<n;i++){try{const a=await r(t,"utf8"),s=parseInt(String(a).trim(),10);if(Number.isFinite(s)&&s>0)return s}catch{}await new Promise(a=>setTimeout(a,100))}return 0}export{P as createMcpHttpLauncher};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class m{defaultTimeoutMs;onTimeout;timers=new Map;constructor(e){this.defaultTimeoutMs=e.defaultTimeoutMs??9e4,this.onTimeout=e.onTimeout}arm(e,t){this.cancel(e);const s=t?.timeoutMs??this.defaultTimeoutMs,i=Date.now()+s,o=setTimeout(()=>{this.timers.delete(e),this.onTimeout(e).catch(()=>{})},s);return this.timers.set(e,o),i}cancel(e){const t=this.timers.get(e);t&&(clearTimeout(t),this.timers.delete(e))}has(e){return this.timers.has(e)}close(){for(const e of this.timers.values())clearTimeout(e);this.timers.clear()}}export{m as ResultTimeoutManager};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{resolveCommandPath as f,spawnCommand as T}from"../../core/runtime/spawn.js";import{createInterface as w}from"node:readline";import{EventEmitter as I}from"node:events";import{formatInboundMessageReferenceText as _}from"../../core/protocol/message-reference.js";import{log as o}from"../../core/log/index.js";import{SessionBindingStore as E}from"../../core/persistence/session-binding-store.js";const S=120*1e3;class y extends I{type="deepseek";config;callbacks;alive=!1;stopped=!1;deepSeekSessionId=null;activeEventId=null;activeSessionId=null;chunkSeq=0;activeClientMsgId=null;idleTimer=null;activeProcess=null;composingTimer=null;composingTTLClear=null;composingTTL=12e4;composingRefreshInterval=3e4;bindingStore=null;aibotSessionId="";cwd;lastUsage=null;currentModel=null;constructor(e,s){super(),this.config=e,this.callbacks=s;const t=e.options??{};if(this.aibotSessionId=String(t.aibotSessionId??"").trim(),this.bindingStore=t.bindingStore instanceof E?t.bindingStore:null,this.cwd=this.resolveCwd(),this.bindingStore&&this.aibotSessionId){const i=this.bindingStore.getDeepSeekThreadId(this.aibotSessionId);i&&(this.deepSeekSessionId=i)}}resolveCwd(){if(this.bindingStore&&this.aibotSessionId){const e=this.bindingStore.get(this.aibotSessionId);if(e?.cwd)return e.cwd}return process.cwd()}async start(){this.alive=!0,this.notifyBindingReady(),o.info("deepseek-adapter","Ready (exec mode)")}async stop(){this.stopped=!0,this.alive=!1,this.stopComposing(),this.clearIdleTimer(),this.killActiveProcess()}isAlive(){return this.alive}async createSession(e){const s=this.deepSeekSessionId??`ds-${Date.now()}`;return this.notifyBindingReady(),s}async resumeSession(e,s){}async destroySession(e){this.deepSeekSessionId=null,this.persistSessionId(void 0)}sendPrompt(e){const s=new k(e.adapterSessionId);return this.runMessage(e,s).catch(t=>{s.emitError(t instanceof Error?t:new Error(String(t)))}),s}async cancel(e){this.killActiveProcess()}setPermissionHandler(e){}async ping(e){return this.alive}getStatus(){return{alive:this.alive,busy:this.activeEventId!==null,sessions:this.deepSeekSessionId?1:0}}getActiveEventIds(){return this.activeEventId?[this.activeEventId]:[]}clearActiveEventForShutdown(){this.clearIdleTimer(),this.killActiveProcess(),this.activeEventId=null}getMcpConfig(){return null}getUsageSnapshot(){return this.lastUsage}getSupportedCommands(){return[{name:"status",description:"Show session and working directory status"}]}async execCommand(e,s,t){return e==="status"?{status:"ok",message:`Session: ${this.deepSeekSessionId??"none"}, CWD: ${this.cwd}`,data:{sessionId:this.deepSeekSessionId,cwd:this.cwd,alive:this.alive}}:{status:"unsupported",message:`Unknown command: ${e}`}}async handleLocalAction(e){const s=e.action_type??"",t=e.params??{};switch(s){case"get_context":return this.callbacks.sendLocalActionResult(e.action_id,"ok",{sessionId:this.deepSeekSessionId,cwd:this.cwd,model:this.currentModel}),{handled:!0,kind:"get_context"};case"set_model":{const i=String(t.model_id??"").trim();return i?(this.currentModel=i,this.callbacks.sendLocalActionResult(e.action_id,"ok",{outcome:"model_set",modelId:i}),o.info("deepseek-adapter",`Model set to: ${i}`),{handled:!0,kind:"set_model"}):(this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"invalid_params","model_id is required"),{handled:!0,kind:"set_model"})}default:return{handled:!1,kind:""}}}deliverInboundEvent(e){const s=_(e.content,{messageId:e.msg_id,quotedMessageId:e.quoted_message_id});if(this.activeEventId){o.info("deepseek-adapter",`Event ${e.event_id}: rejected, busy with ${this.activeEventId}`),this.callbacks.sendEventResult(e.event_id,"failed","agent busy");return}this.startNewMessage(e,s)}deliverStopEvent(e,s){this.activeEventId===e&&(this.callbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActive())}startNewMessage(e,s){this.activeEventId=e.event_id,this.activeSessionId=e.session_id,this.chunkSeq=0,this.activeClientMsgId=`ds-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,this.startComposing();const t={adapterSessionId:this.deepSeekSessionId??"",text:s,contextMessages:e.context_messages_json?JSON.parse(e.context_messages_json).map(n=>({senderId:n.sender_id??"unknown",content:n.content})):void 0},i=new k(this.deepSeekSessionId??"");this.runMessage(t,i,e.event_id,e.session_id).catch(n=>{o.error("deepseek-adapter",`Message failed: ${n}`),this.callbacks.sendEventResult(e.event_id,"failed",n instanceof Error?n.message:String(n)),this.clearActive()}),this.resetIdleTimer(e.event_id)}buildExecArgs(e){const s=["exec","--output-format","stream-json"];return this.currentModel&&s.push("--model",this.currentModel),this.deepSeekSessionId&&s.push("--resume",this.deepSeekSessionId),s.push("--",e),s}async runMessage(e,s,t,i){let n=e.text;e.contextMessages&&e.contextMessages.length>0&&(n=`Conversation context:
|
|
2
|
+
${e.contextMessages.map(c=>`[${c.senderId??"unknown"}]: ${c.content}`).join(`
|
|
3
|
+
`)}
|
|
4
|
+
|
|
5
|
+
Latest user message:
|
|
6
|
+
${n}`);const h=this.config.command||"codewhale",d=this.buildExecArgs(n),u={...process.env,...this.config.env},g=f(h,typeof u.PATH=="string"?u.PATH:void 0);o.info("deepseek-adapter",`Spawning: ${g} ${d.slice(0,5).join(" ")}...`);const r=T(g,d,{cwd:this.cwd,env:u}).process;return this.activeProcess=r,r.stderr?.on("data",c=>{const l=c.toString().trim();l&&o.info("deepseek-adapter",`[deepseek stderr] ${l}`)}),new Promise((c,l)=>{let p=!1,m="";const v=()=>{this.activeProcess=null};r.on("error",a=>{p||(p=!0,v(),l(a))}),r.on("exit",a=>{if(m.trim()&&this.handleOutputLine(m.trim(),t),m="",p){v();return}if(p=!0,v(),a!==0&&t&&this.activeEventId===t){l(new Error(`deepseek exec exited with code ${a}`));return}s.emitDone({status:"completed"}),c()}),w({input:r.stdout}).on("line",a=>{a.trim()&&this.handleOutputLine(a.trim(),t)}),r.stdin?.end()})}handleOutputLine(e,s){let t;try{t=JSON.parse(e)}catch{o.error("deepseek-adapter",`Invalid JSON: ${e.slice(0,200)}`);return}switch(t.type){case"content":{const i=t.content;i&&s&&this.activeEventId===s&&this.activeSessionId&&(this.chunkSeq++,this.callbacks.sendStreamChunk(s,this.activeSessionId,i,this.chunkSeq,!1,this.activeClientMsgId??void 0),this.startComposing(),this.resetIdleTimer(s));break}case"session_capture":{const i=t.content;i&&(this.deepSeekSessionId=i,this.persistSessionId(i),o.info("deepseek-adapter",`Session captured: ${i}`));break}case"metadata":{const i=t.meta;if(i){o.info("deepseek-adapter",`Metadata: model=${i.model}, tokens_in=${i.input_tokens}, tokens_out=${i.output_tokens}`);const n=Number(i.input_tokens??0),h=Number(i.output_tokens??0);if(n>0||h>0){const d=this.lastUsage;this.lastUsage={sampledAt:new Date().toISOString(),turns:(d?.turns??0)+1,total:{input:(d?.total.input??0)+n,output:(d?.total.output??0)+h}}}}break}case"tool_use":{const i=t.name,n=typeof t.input=="string"?t.input:JSON.stringify(t.input??{});i&&s&&this.activeEventId===s&&this.activeSessionId&&(o.info("deepseek-adapter",`Tool use: ${i}`),this.callbacks.sendToolUse(s,this.activeSessionId,i,n),this.resetIdleTimer(s));break}case"tool_result":{const i=t.name,n=t.output;s&&this.activeEventId===s&&this.activeSessionId&&(this.callbacks.sendToolResult(s,this.activeSessionId,i??"unknown",n??""),this.resetIdleTimer(s));break}case"done":{this.handleMessageCompleted(s);break}default:break}}handleMessageCompleted(e){if(this.stopComposing(),e&&this.activeEventId===e){const s=this.activeSessionId??"",t=this.activeClientMsgId??void 0;s&&(this.chunkSeq++,this.callbacks.sendStreamChunk(e,s,"",this.chunkSeq,!0,t)),this.callbacks.sendEventResult(e,"responded"),this.clearActive()}}killActiveProcess(){const e=this.activeProcess;if(this.activeProcess=null,e?.pid)try{e.kill("SIGTERM")}catch{}}notifyBindingReady(){!this.aibotSessionId||!this.cwd||this.callbacks.sendUpdateBindingCard(this.aibotSessionId,"ready",this.cwd)}persistSessionId(e){!this.bindingStore||!this.aibotSessionId||this.bindingStore.setDeepSeekThreadId(this.aibotSessionId,e)}startComposing(){if(!this.activeSessionId||this.composingTimer)return;this.stopComposing();const e=this.activeSessionId,s={ttl_ms:this.composingTTL};this.callbacks.sendSessionActivitySet(e,"composing",!0,s),this.composingTimer=setInterval(()=>{this.callbacks.sendSessionActivitySet(e,"composing",!0,s)},this.composingRefreshInterval),this.composingTTLClear=setTimeout(()=>{this.stopComposing()},this.composingTTL)}stopComposing(){this.composingTimer&&(clearInterval(this.composingTimer),this.composingTimer=null),this.composingTTLClear&&(clearTimeout(this.composingTTLClear),this.composingTTLClear=null),this.activeSessionId&&this.callbacks.sendSessionActivitySet(this.activeSessionId,"composing",!1)}resetIdleTimer(e){this.clearIdleTimer(),this.idleTimer=setTimeout(()=>{this.activeEventId===e&&(o.error("deepseek-adapter",`Agent idle for ${S/1e3}s: ${e}`),this.killActiveProcess(),this.callbacks.sendEventResult(e,"failed",`agent idle for ${S/1e3}s`),this.clearActive(),this.emit("stuck"))},S)}clearIdleTimer(){this.idleTimer&&(clearTimeout(this.idleTimer),this.idleTimer=null)}clearActive(){const e=this.activeEventId;this.stopComposing(),this.activeEventId=null,this.activeSessionId=null,this.chunkSeq=0,this.activeClientMsgId=null,this.clearIdleTimer(),e&&this.emit("eventDone",e)}}class k extends I{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){o.warn("deepseek-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}export{y as DeepSeekAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DeepSeekAdapter as e}from"./deepseek-adapter.js";export{e as DeepSeekAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{QwenAdapter as e}from"./qwen-adapter.js";export{e as QwenAdapter};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import l from"node:path";import{fileURLToPath as m}from"node:url";import{EventEmitter as h}from"node:events";import{AgentProcess as f}from"../../agent/process.js";import{AcpClient as v,AcpAuthRequiredError as g}from"../../protocol/acp-client.js";import{AgentEventType as a}from"../../types/events.js";import{QuotedMessageStream as R}from"../../core/util/quoted-message-stream.js";import{InternalApiServer as A}from"../../core/mcp/internal-api-server.js";import{EventResultsStore as b}from"../../core/persistence/event-results-store.js";import{log as r}from"../../core/log/index.js";const d=l.dirname(m(import.meta.url)),w=300*1e3,u=60*1e3,I=200;class T extends h{type="qwen";config;callbacks;agentProcess=null;acpClient=null;internalApi=null;activeRun=null;pendingApprovals=new Map;bindingStore;eventResults=null;currentAibotSessionId;stopped=!1;cwd;model;promptTimeoutMs;clientMsgSeq=0;deferredEvents=new Map;sessionBindings=new Map;constructor(e,t,s,i){super(),this.config=e,this.callbacks=t,this.bindingStore=s,this.cwd=e.cwd??process.cwd(),this.model=e.options?.model,this.promptTimeoutMs=e.options?.promptTimeoutMs??w,i&&(this.eventResults=new b(i))}async start(){const e=[...this.config.args??[],"--acp"];this.model&&e.push("--model",this.model);const t={command:this.config.command||"qwen",args:e,cwd:this.cwd,env:this.config.env},s=await this.startInternalApiAndMcp();this.agentProcess=new f;const i=await this.agentProcess.start(t);r.info("qwen-adapter","Qwen process started"),this.agentProcess.on("exit",n=>{this.stopped||(r.error("qwen-adapter",`Process exited unexpectedly (code=${n})`),this.activeRun&&this.finishRun("failed",`qwen process exited (code=${n})`),this.emit("exit",n))}),this.acpClient=new v,this.acpClient.on("event",n=>this.handleAcpEvent(n));const o=this.currentAibotSessionId?this.bindingStore.getAcpSessionId(this.currentAibotSessionId):void 0;try{await this.acpClient.connect({transport:i,initialMode:"bypass",mcpServers:s,sessionId:o,cwd:this.cwd}),r.info("qwen-adapter",`ACP session ready: ${this.acpClient.sessionId}`),this.currentAibotSessionId&&this.bindingStore.setAcpSessionId(this.currentAibotSessionId,this.acpClient.sessionId);for(const[n,c]of this.sessionBindings)this.callbacks.sendUpdateBindingCard(n,"ready",c)}catch(n){if(n instanceof g){await this.handleAuthRequired(n);return}throw n}}async stop(){this.stopped=!0,this.deferredEvents.clear(),this.activeRun&&(this.activeRun.flushTimer&&(clearTimeout(this.activeRun.flushTimer),this.activeRun.flushTimer=null),this.activeRun.timeoutTimer&&(clearTimeout(this.activeRun.timeoutTimer),this.activeRun.timeoutTimer=null),this.activeRun.firstResponseTimer&&(clearTimeout(this.activeRun.firstResponseTimer),this.activeRun.firstResponseTimer=null),this.activeRun=null),this.acpClient&&(this.acpClient.removeAllListeners(),this.acpClient=null),this.agentProcess&&(await this.agentProcess.close(),this.agentProcess=null),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.acpClient?.isAlive??!1}async createSession(e){return this.acpClient?.sessionId??""}async resumeSession(e,t){}async destroySession(e){}sendPrompt(e){const t=new S(e.adapterSessionId);return this.acpClient?.isAlive&&this.acpClient.send(e.text).catch(s=>{t.emitError(s instanceof Error?s:new Error(String(s)))}),t}async cancel(e){this.activeRun&&this.acpClient&&(await this.acpClient.cancel(),this.flushStream(),this.finishRun("canceled","stopped by user"))}setPermissionHandler(e){}async ping(e){return this.acpClient?.ping(e)??!1}getStatus(){return{alive:this.acpClient?.isAlive??!1,busy:this.activeRun!==null,sessions:this.acpClient?1:0}}getMcpConfig(){if(!this.internalApi)return null;const e=l.resolve(d,"../../mcp/acp-mcp-server.js");return{name:"grix-connector-tools",command:process.execPath,args:[e,"--api-url",this.internalApi.url]}}get pendingApprovalEntries(){return this.pendingApprovals}get acpSessionOptions(){return this.acpClient?.sessionOptions??null}async handleLocalAction(e){const t=e.action_type??"",s=e.params??{};if(t==="exec_approve"||t==="exec_reject"){const i=String(s.tool_call_id??""),o=t==="exec_approve";if(!i)return this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"tool_call_id_required","tool_call_id is required"),{handled:!0,kind:"approval"};const n=this.pendingApprovals.get(i);return n?(this.pendingApprovals.delete(i),this.acpClient&&this.acpClient.respondPermission(n,{behavior:o?"allow":"deny"}).catch(c=>{r.error("qwen-adapter",`Failed to respond to permission: ${c}`)}),this.callbacks.sendLocalActionResult(e.action_id,"ok"),{handled:!0,kind:"approval"}):(this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_not_found",`no pending approval for tool_call_id: ${i}`),{handled:!0,kind:"approval"})}return{handled:!1,kind:""}}async startInternalApiAndMcp(){try{this.internalApi=new A,this.internalApi.setInvokeHandler(async(t,s)=>this.callbacks.agentInvoke(t,s)),await this.internalApi.start(0),r.info("qwen-adapter",`Internal API started at ${this.internalApi.url}`);const e=l.resolve(d,"../../mcp/acp-mcp-server.js");return[{name:"grix-connector-tools",command:process.execPath,args:[e,"--api-url",this.internalApi.url],env:{GRIX_CONNECTOR_INTERNAL_API:this.internalApi.url}}]}catch(e){r.error("qwen-adapter",`Failed to start MCP tools: ${e}`);return}}bindSession(e,t){return this.sessionBindings.get(e)?!1:(this.sessionBindings.set(e,t),this.bindingStore.set(e,t),this.acpClient?.sessionId&&this.bindingStore.setAcpSessionId(e,this.acpClient.sessionId),this.acpClient?.isAlive&&this.callbacks.sendUpdateBindingCard(e,"connected",t),!0)}getSessionCwd(e){return this.sessionBindings.get(e)}getSessionBindings(){return this.sessionBindings}deliverInboundEvent(e){if(this.callbacks.sendEventAck(e.event_id,e.session_id),this.eventResults?.has(e.session_id,e.event_id)){const t=this.eventResults.get(e.session_id,e.event_id);r.info("qwen-adapter",`Deduplicating event ${e.event_id} (cached: ${t.status})`),this.callbacks.sendEventResult(e.event_id,t.status,t.msg);return}if(this.activeRun){r.info("qwen-adapter",`Event ${e.event_id} rejected: busy`),this.callbacks.sendEventResult(e.event_id,"failed","agent busy");return}if(!this.acpClient?.isAlive){this.callbacks.sendEventResult(e.event_id,"failed","qwen agent not alive");return}if(e.session_id&&e.session_id!==this.currentAibotSessionId&&(this.currentAibotSessionId=e.session_id),!this.sessionBindings.has(e.session_id)){this.deferEvent(e),this.callbacks.sendStreamChunk(e.event_id,e.session_id,"Qwen needs a workspace before it can reply. Use /grix open <directory> to bind.",1,!1),this.callbacks.sendStreamChunk(e.event_id,e.session_id,"",2,!0),this.callbacks.sendEventResult(e.event_id,"responded");return}this.startRun(e,!1)}deliverStopEvent(e){this.activeRun?.eventId===e&&(this.flushStream(),this.finishRun("canceled","stopped by user"))}deferEvent(e){const t=this.deferredEvents.get(e.session_id)??[];t.some(s=>s.event.event_id===e.event_id)||(t.push({event:e,queuedAt:Date.now()}),this.deferredEvents.set(e.session_id,t),r.info("qwen-adapter",`Deferred event ${e.event_id} for session ${e.session_id} (queue: ${t.length})`))}replayDeferredEvents(e){const t=this.deferredEvents.get(e);if(!(!t||t.length===0)){this.deferredEvents.delete(e),r.info("qwen-adapter",`Replaying ${t.length} deferred events for session ${e}`);for(const{event:s}of t){if(this.activeRun){r.info("qwen-adapter",`Cannot replay ${s.event_id}: agent busy, dropping`);continue}this.startRun(s,!0)}}}startRun(e,t){this.activeRun={eventId:e.event_id,sessionId:e.session_id,threadId:e.thread_id,clientMsgId:`qwen_${++this.clientMsgSeq}_${Date.now()}`,chunkSeq:0,buffer:"",quotedStream:new R,responded:!1,silent:t,flushTimer:null,timeoutTimer:null,firstResponseTimer:null};const s=this.activeRun;s.firstResponseTimer=setTimeout(()=>{this.activeRun?.eventId===e.event_id&&!s.responded&&(r.error("qwen-adapter",`No response from agent within ${u}ms for ${e.event_id}`),this.finishRun("failed","agent not responding"))},u),s.timeoutTimer=setTimeout(()=>{this.activeRun?.eventId===e.event_id&&(r.error("qwen-adapter",`Prompt timed out for ${e.event_id}`),this.finishRun("failed","agent response timed out"))},this.promptTimeoutMs),this.callbacks.sendSessionComposing(e.session_id,!0),this.acpClient.send(e.content).catch(i=>{r.error("qwen-adapter",`Prompt failed: ${i}`),this.finishRun("failed",i instanceof Error?i.message:String(i))})}handleAcpEvent(e){if(e.type===a.PermissionRequest){this.handlePermissionRequest(e);return}const t=this.activeRun;if(t)switch(t.responded||(t.responded=!0,t.firstResponseTimer&&(clearTimeout(t.firstResponseTimer),t.firstResponseTimer=null)),e.type){case a.Text:{e.content&&this.appendToStream(t,e.content);break}case a.ToolUse:{e.toolName&&this.callbacks.sendToolUse(t.eventId,t.sessionId,e.toolName,e.toolInput??"");break}case a.ToolResult:{e.content&&this.callbacks.sendToolResult(t.eventId,t.sessionId,e.content);break}case a.Thinking:{e.content&&this.callbacks.sendThinking(t.eventId,t.sessionId,e.content);break}case a.Error:{r.error("qwen-adapter",`ACP error: ${e.error}`);break}case a.Result:{this.flushStream(),this.finishRun("responded");break}}}handlePermissionRequest(e){const t=e.permissionRequest;if(!t||!e.requestId||!this.acpClient)return;const s=t.toolCallId;this.pendingApprovals.set(s,e.requestId);const i=this.activeRun;i?this.callbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,toolCallId:s,toolName:t.toolName,toolTitle:t.toolTitle,options:t.options}):(r.info("qwen-adapter",`Permission request without active run, auto-approving: ${t.toolName}`),this.acpClient.respondPermission(e.requestId,{behavior:"allow"}),this.pendingApprovals.delete(s))}async handleAuthRequired(e){r.info("qwen-adapter",`Auth required, methods: ${e.authMethods.map(o=>o.id).join(", ")}`);const t=e.authMethods.find(o=>/oauth|browser/i.test(o.id))??e.authMethods[0];if(!t)throw e;const s=this.currentAibotSessionId??"";this.callbacks.sendAuthNotification(s,`Qwen authentication required (${t.id}). Initiating auth flow...`);for(const[o,n]of this.sessionBindings)this.callbacks.sendUpdateBindingCard(o,"failed",n);const i=await this.captureAuthUrl();i&&this.callbacks.sendAuthNotification(s,`Please open this URL to authenticate:
|
|
2
|
+
${i}
|
|
3
|
+
|
|
4
|
+
Waiting for authentication to complete...`);try{await this.acpClient.authenticate(t.id),r.info("qwen-adapter","Authentication successful"),this.callbacks.sendAuthNotification(s,"Authentication successful. Resuming..."),await this.acpClient.connect({transport:this.agentProcess.transport,initialMode:"bypass",mcpServers:this.internalApi?[{name:"grix-connector-tools",command:process.execPath,args:[l.resolve(d,"../../mcp/acp-mcp-server.js"),"--api-url",this.internalApi.url],env:{GRIX_CONNECTOR_INTERNAL_API:this.internalApi.url}}]:void 0}),r.info("qwen-adapter",`ACP session ready after auth: ${this.acpClient.sessionId}`);for(const[o,n]of this.sessionBindings)this.callbacks.sendUpdateBindingCard(o,"ready",n)}catch(o){throw r.error("qwen-adapter",`Auth retry failed: ${o}`),o}}captureAuthUrl(){return new Promise(e=>{if(!this.agentProcess){e(null);return}const t=/https?:\/\/[^\s"')\]]+/;let s=!1;const i=o=>{if(s)return;const n=o.toString().replace(/\x1b\[[0-9;]*m/g,"").match(t);n&&(s=!0,this.agentProcess.removeListener("stderr",i),e(n[0]))};this.agentProcess.on("stderr",i),setTimeout(()=>{s||(s=!0,this.agentProcess.removeListener("stderr",i),e(null))},3e4)})}appendToStream(e,t){const s=e.quotedStream.consume(t);s.deltaContent&&(e.buffer+=s.deltaContent,e.flushTimer||(e.flushTimer=setTimeout(()=>this.flushStream(),I)))}flushStream(){const e=this.activeRun;if(!e||!e.buffer)return;e.flushTimer&&(clearTimeout(e.flushTimer),e.flushTimer=null);const t=e.buffer;e.buffer="",this.callbacks.sendStreamChunk(e.eventId,e.sessionId,t,++e.chunkSeq,!1)}finishRun(e,t){const s=this.activeRun;if(!s)return;this.activeRun=null,this.callbacks.sendSessionComposing(s.sessionId,!1),s.flushTimer&&(clearTimeout(s.flushTimer),s.flushTimer=null),s.timeoutTimer&&(clearTimeout(s.timeoutTimer),s.timeoutTimer=null),s.firstResponseTimer&&(clearTimeout(s.firstResponseTimer),s.firstResponseTimer=null);const i=s.quotedStream.flush();i.deltaContent&&(s.buffer+=i.deltaContent),s.buffer&&(this.callbacks.sendStreamChunk(s.eventId,s.sessionId,s.buffer,++s.chunkSeq,!1),s.buffer=""),t&&this.callbacks.sendRunError(s.eventId,s.sessionId,t),this.callbacks.sendStreamChunk(s.eventId,s.sessionId,"",++s.chunkSeq,!0),s.silent||this.callbacks.sendEventResult(s.eventId,e,t),this.eventResults&&!s.silent&&this.eventResults.set({sessionId:s.sessionId,eventId:s.eventId,status:e,msg:t,updatedAt:Date.now()})}}class S extends h{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){this.emit("error",e)}async cancel(){}}export{T as QwenAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{EventEmitter as o}from"node:events";import c from"ws";const r="aibot-agent-api-v1",h=1;class l extends o{ws=null;seq=0;heartbeatTimer=null;heartbeatSec=30;connected=!1;config;constructor(e){super(),this.config={url:e.url,agentId:e.agentId,apiKey:e.apiKey,clientType:e.clientType,capabilities:e.capabilities??["stream_chunk","local_action_v1"],localActions:e.localActions??["exec_approve","exec_reject"]}}get isConnected(){return this.connected}async connect(){return new Promise((e,a)=>{const s=new c(this.config.url);this.ws=s;const i=setTimeout(()=>{a(new Error("Auth timeout: no auth_ack received within 15s")),s.close()},15e3);s.on("open",()=>{this.sendPacket("auth",{agent_id:this.config.agentId,api_key:this.config.apiKey,client_type:this.config.clientType,protocol_version:r,contract_version:h,capabilities:this.config.capabilities,local_actions:this.config.localActions})}),s.on("message",t=>{let n;try{n=JSON.parse(t.toString())}catch{return}this.handlePacket(n,i,e,a)}),s.on("close",(t,n)=>{this.connected=!1,this.stopHeartbeat(),clearTimeout(i),this.emit("close",t,n.toString())}),s.on("error",t=>{clearTimeout(i),this.emit("error",t),this.connected||a(t)})})}handlePacket(e,a,s,i){switch(e.cmd){case"auth_ack":{clearTimeout(a);const t=e.payload;t.code===0?(this.connected=!0,t.heartbeat_sec&&(this.heartbeatSec=t.heartbeat_sec),this.startHeartbeat(),this.emit("auth",t),s(t)):i(new Error(`Auth failed: code=${t.code} msg=${t.msg}`));break}case"ping":{this.sendPacket("pong",e.payload??{});break}case"event_msg":{this.emit("event",e.payload);break}case"local_action":{this.emit("localAction",e.payload);break}case"event_stop":{this.emit("stop",e.payload);break}case"kicked":{this.emit("kicked",e.payload),this.disconnect();break}case"error":{const t=e.payload;this.emit("error",new Error(`Server error: code=${t.code} msg=${t.msg}`));break}case"send_ack":case"send_nack":case"local_action_ack":break;default:break}}sendEventAck(e){this.sendPacket("event_ack",e)}sendStreamChunk(e){this.sendPacket("client_stream_chunk",e)}sendMsg(e){this.sendPacket("send_msg",e)}sendEventResult(e){this.sendPacket("event_result",e)}sendLocalActionResult(e){this.sendPacket("local_action_result",e)}sendEventStopAck(e){this.sendPacket("event_stop_ack",e)}sendEventStopResult(e){this.sendPacket("event_stop_result",e)}sendPing(){this.sendPacket("ping",{})}disconnect(){this.connected=!1,this.stopHeartbeat(),this.ws&&(this.ws.close(),this.ws=null)}sendPacket(e,a){if(!this.ws||this.ws.readyState!==c.OPEN)return;const s={cmd:e,seq:++this.seq,payload:a};this.ws.send(JSON.stringify(s))}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.connected&&this.sendPing()},this.heartbeatSec*1e3)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}}export{l as AibotClient};
|