@dhf-hermes/grix 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @dhf-hermes/grix might be problematic. Click here for more details.
- package/.gitignore +6 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/grix-hermes.mjs +93 -0
- package/grix-admin/SKILL.md +109 -0
- package/grix-admin/agents/openai.yaml +7 -0
- package/grix-admin/scripts/admin.mjs +12 -0
- package/grix-admin/scripts/bind_from_json.py +118 -0
- package/grix-admin/scripts/bind_local.py +226 -0
- package/grix-egg/SKILL.md +73 -0
- package/grix-egg/agents/openai.yaml +7 -0
- package/grix-egg/references/acceptance-checklist.md +10 -0
- package/grix-egg/scripts/card-link.mjs +12 -0
- package/grix-egg/scripts/validate_install_context.mjs +74 -0
- package/grix-group/SKILL.md +42 -0
- package/grix-group/agents/openai.yaml +7 -0
- package/grix-group/scripts/group.mjs +12 -0
- package/grix-query/SKILL.md +53 -0
- package/grix-query/agents/openai.yaml +7 -0
- package/grix-query/scripts/query.mjs +12 -0
- package/grix-register/SKILL.md +68 -0
- package/grix-register/agents/openai.yaml +7 -0
- package/grix-register/references/handoff-contract.md +21 -0
- package/grix-register/scripts/create_api_agent_and_bind.py +105 -0
- package/grix-register/scripts/grix_auth.py +487 -0
- package/grix-update/SKILL.md +50 -0
- package/grix-update/agents/openai.yaml +7 -0
- package/grix-update/references/cron-setup.md +11 -0
- package/grix-update/scripts/grix_update.py +99 -0
- package/lib/manifest.mjs +68 -0
- package/message-send/SKILL.md +71 -0
- package/message-send/agents/openai.yaml +7 -0
- package/message-send/scripts/card-link.mjs +40 -0
- package/message-send/scripts/send.mjs +12 -0
- package/message-unsend/SKILL.md +39 -0
- package/message-unsend/agents/openai.yaml +7 -0
- package/message-unsend/scripts/unsend.mjs +12 -0
- package/openclaw-memory-setup/SKILL.md +38 -0
- package/openclaw-memory-setup/agents/openai.yaml +7 -0
- package/openclaw-memory-setup/scripts/bench_ollama_embeddings.py +257 -0
- package/openclaw-memory-setup/scripts/set_openclaw_memory_model.py +240 -0
- package/openclaw-memory-setup/scripts/survey_host_readiness.py +379 -0
- package/package.json +51 -0
- package/shared/cli/actions.mjs +339 -0
- package/shared/cli/aibot-client.mjs +274 -0
- package/shared/cli/card-links.mjs +90 -0
- package/shared/cli/config.mjs +141 -0
- package/shared/cli/grix-hermes.mjs +87 -0
- package/shared/cli/targets.mjs +119 -0
- package/shared/references/grix-card-links.md +27 -0
- package/shared/references/hermes-grix-config.md +30 -0
package/.gitignore
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# grix-hermes
|
|
2
|
+
|
|
3
|
+
`grix-hermes` 是一个独立发布的 Hermes 技能包项目。
|
|
4
|
+
它对外发布到 npm 的包名是 `@dhf-hermes/grix`,安装后的命令仍然是 `grix-hermes`。
|
|
5
|
+
|
|
6
|
+
它的目标很单一:
|
|
7
|
+
|
|
8
|
+
- 不修改 Hermes 内核
|
|
9
|
+
- 复用 Hermes 已有的 `terminal` 和 `send_message`
|
|
10
|
+
- 把 Grix / OpenClaw 相关技能做成可单独安装、可 npm 发布、可 GitHub 发布的技能包
|
|
11
|
+
|
|
12
|
+
## 包含内容
|
|
13
|
+
|
|
14
|
+
- 9 个迁移后的技能
|
|
15
|
+
- 一套共享 Grix WS CLI,给 `grix-query`、`grix-group`、`grix-admin`、`message-unsend` 使用
|
|
16
|
+
- 独立 HTTP 组件,给 `grix-register` 使用
|
|
17
|
+
- OpenClaw 本地运维脚本,给 `openclaw-memory-setup` 使用
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
### 方式 1:npm
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @dhf-hermes/grix
|
|
25
|
+
grix-hermes install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
默认会安装到:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
~/.hermes/skills/grix-hermes
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
如果你用了自定义 `HERMES_HOME`,安装器会跟随它。
|
|
35
|
+
|
|
36
|
+
### 方式 2:GitHub / 本地目录
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone <repo> /path/to/grix-hermes
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
然后可以二选一:
|
|
43
|
+
|
|
44
|
+
1. 直接把仓库根目录作为 `skills.external_dirs`
|
|
45
|
+
2. 执行 `node ./bin/grix-hermes.mjs install --dest <目标目录>`
|
|
46
|
+
|
|
47
|
+
## 命令
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
grix-hermes list
|
|
51
|
+
grix-hermes manifest
|
|
52
|
+
grix-hermes install
|
|
53
|
+
grix-hermes install --dest ~/.hermes/skills/grix-hermes --force
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## GitHub / npm 发布
|
|
57
|
+
|
|
58
|
+
仓库已经带了两条工作流:
|
|
59
|
+
|
|
60
|
+
- `.github/workflows/ci.yml`
|
|
61
|
+
- `.github/workflows/publish.yml`
|
|
62
|
+
|
|
63
|
+
发布时只需要准备:
|
|
64
|
+
|
|
65
|
+
1. GitHub 仓库
|
|
66
|
+
2. npm 包名:`@dhf-hermes/grix`
|
|
67
|
+
3. GitHub Secret: `NPM_TOKEN`
|
|
68
|
+
|
|
69
|
+
仓库里也带了本地发布脚本:
|
|
70
|
+
|
|
71
|
+
- `./publish.sh`
|
|
72
|
+
- `./scripts/publish_npm.sh`
|
|
73
|
+
|
|
74
|
+
常用方式:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bash ./publish.sh
|
|
78
|
+
bash ./publish.sh --publish
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 设计边界
|
|
82
|
+
|
|
83
|
+
- Grix 查询、群管理、远端 Agent 管理、消息撤回:走 Grix WS 协议
|
|
84
|
+
- 注册、发验证码、创建首个 API agent:走独立 HTTP 组件
|
|
85
|
+
- 发消息、发卡片:优先使用 Hermes 自带 `send_message`
|
|
86
|
+
- 本项目不依赖修改 Hermes 内核,也不要求给 Hermes 增加新 tool
|
|
87
|
+
|
|
88
|
+
## 技能清单
|
|
89
|
+
|
|
90
|
+
- `grix-admin`
|
|
91
|
+
- `grix-egg`
|
|
92
|
+
- `grix-group`
|
|
93
|
+
- `grix-query`
|
|
94
|
+
- `grix-register`
|
|
95
|
+
- `grix-update`
|
|
96
|
+
- `message-send`
|
|
97
|
+
- `message-unsend`
|
|
98
|
+
- `openclaw-memory-setup`
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { defaultInstallDir, installEntries, manifestData, projectRoot, SKILLS } from "../lib/manifest.mjs";
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`grix-hermes
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
grix-hermes list
|
|
12
|
+
grix-hermes manifest
|
|
13
|
+
grix-hermes install [--dest <dir>] [--force]
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const positional = [];
|
|
19
|
+
const flags = {};
|
|
20
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
21
|
+
const token = argv[i];
|
|
22
|
+
if (!token.startsWith("--")) {
|
|
23
|
+
positional.push(token);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const key = token.slice(2);
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
if (!next || next.startsWith("--")) {
|
|
29
|
+
flags[key] = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
flags[key] = next;
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
return { positional, flags };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyRecursive(src, dest, force) {
|
|
39
|
+
if (fs.existsSync(dest)) {
|
|
40
|
+
if (!force) {
|
|
41
|
+
throw new Error(`Destination already exists: ${dest}. Use --force to overwrite.`);
|
|
42
|
+
}
|
|
43
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
46
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runInstall(flags) {
|
|
50
|
+
const root = projectRoot();
|
|
51
|
+
const destRoot = path.resolve(String(flags.dest || defaultInstallDir()));
|
|
52
|
+
fs.mkdirSync(destRoot, { recursive: true });
|
|
53
|
+
for (const entry of installEntries()) {
|
|
54
|
+
copyRecursive(path.join(root, entry), path.join(destRoot, entry), Boolean(flags.force));
|
|
55
|
+
}
|
|
56
|
+
console.log(destRoot);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runList() {
|
|
60
|
+
for (const skill of SKILLS) {
|
|
61
|
+
console.log(`${skill.name}\t${skill.description}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runManifest() {
|
|
66
|
+
console.log(JSON.stringify(manifestData(), null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
70
|
+
const command = positional[0] || "help";
|
|
71
|
+
|
|
72
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
73
|
+
printHelp();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (command === "list") {
|
|
78
|
+
runList();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (command === "manifest") {
|
|
83
|
+
runManifest();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (command === "install") {
|
|
88
|
+
runInstall(flags);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
printHelp();
|
|
93
|
+
process.exit(1);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grix-admin
|
|
3
|
+
description: 需要创建远端 Grix API agent、管理分类、并把结果落地到本地 OpenClaw 配置时使用。适用于首个 agent 绑定、后续创建并绑定 agent、分类管理。远端步骤通过 `../shared/cli/grix-hermes.mjs admin`,本地步骤通过 `openclaw` 官方 CLI。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Grix Admin
|
|
7
|
+
|
|
8
|
+
这个技能负责两件事:
|
|
9
|
+
|
|
10
|
+
1. 远端 Grix agent / 分类管理
|
|
11
|
+
2. 本地 OpenClaw 绑定和校验
|
|
12
|
+
|
|
13
|
+
不要手工改 `openclaw.json`。
|
|
14
|
+
|
|
15
|
+
## Mode A: bind-local
|
|
16
|
+
|
|
17
|
+
当上下文已经给出:
|
|
18
|
+
|
|
19
|
+
- `agent_name`
|
|
20
|
+
- `agent_id`
|
|
21
|
+
- `api_endpoint`
|
|
22
|
+
- `api_key`
|
|
23
|
+
|
|
24
|
+
就直接做本地绑定,不做远端创建。
|
|
25
|
+
|
|
26
|
+
### 本地绑定主线
|
|
27
|
+
|
|
28
|
+
1. 先用 `openclaw config get --json` 读取:
|
|
29
|
+
- `channels.grix.accounts`
|
|
30
|
+
- `agents.list`
|
|
31
|
+
- `tools.profile`
|
|
32
|
+
- `tools.alsoAllow`
|
|
33
|
+
- `tools.sessions.visibility`
|
|
34
|
+
2. 准备目标值:
|
|
35
|
+
- `channels.grix.accounts.<agent_name>`
|
|
36
|
+
- `agents.list` 里目标 agent
|
|
37
|
+
- `tools.profile="coding"`
|
|
38
|
+
- `tools.alsoAllow` 至少包含 `message`、`grix_register`
|
|
39
|
+
3. 用官方命令写入:
|
|
40
|
+
- `openclaw config set ... --strict-json`
|
|
41
|
+
- `openclaw agents bind --agent <agent_name> --bind grix:<agent_name>`
|
|
42
|
+
4. 校验:
|
|
43
|
+
- `openclaw config validate`
|
|
44
|
+
- `openclaw config get --json channels.grix.accounts.<agent_name>`
|
|
45
|
+
- `openclaw agents bindings --agent <agent_name> --json`
|
|
46
|
+
|
|
47
|
+
### model 规则
|
|
48
|
+
|
|
49
|
+
- 先复用该 agent 现有 `model`
|
|
50
|
+
- 没有则看 `agents.defaults.model.primary`
|
|
51
|
+
- 还没有就停止,不要猜
|
|
52
|
+
|
|
53
|
+
## Mode B: create-and-bind
|
|
54
|
+
|
|
55
|
+
如果还没有远端 agent,就先创建。
|
|
56
|
+
|
|
57
|
+
### 远端创建
|
|
58
|
+
|
|
59
|
+
通过 `terminal` 执行:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node scripts/admin.mjs --action create_agent --agent-name <NAME> [--introduction ...] [--is-main true|false]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
如果需要分类:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node scripts/admin.mjs --action create_agent --agent-name <NAME> --category-id <ID>
|
|
69
|
+
node scripts/admin.mjs --action create_agent --agent-name <NAME> --category-name <NAME> --parent-category-id 0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
创建成功后,拿返回里的 `id` / `agent_name` / `api_endpoint` / `api_key`,继续走本地绑定 helper:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python3 scripts/bind_local.py \
|
|
76
|
+
--agent-name <AGENT_NAME> \
|
|
77
|
+
--agent-id <AGENT_ID> \
|
|
78
|
+
--api-endpoint <WS_URL> \
|
|
79
|
+
--api-key <API_KEY> \
|
|
80
|
+
--model <MODEL>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
如果你已经拿到一份远端创建结果 JSON,可以直接交给:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python3 scripts/bind_from_json.py --model <MODEL> --dry-run --json < result.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Mode C: category-manage
|
|
90
|
+
|
|
91
|
+
分类相关动作统一走:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
node scripts/admin.mjs --action list_categories
|
|
95
|
+
node scripts/admin.mjs --action create_category --name <NAME> --parent-id 0
|
|
96
|
+
node scripts/admin.mjs --action update_category --category-id <ID> --name <NAME> --parent-id 0
|
|
97
|
+
node scripts/admin.mjs --action assign_category --agent-id <AGENT_ID> --category-id <CATEGORY_ID>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Guardrails
|
|
101
|
+
|
|
102
|
+
- 远端动作不要改走 HTTP
|
|
103
|
+
- 本地配置不要手工编辑 JSON
|
|
104
|
+
- 安装私聊进行中时,不要主动重启 gateway
|
|
105
|
+
- 只有配置已确认正确但运行态明显是旧结果时,才把 `openclaw gateway restart` 当补救
|
|
106
|
+
|
|
107
|
+
## 参考
|
|
108
|
+
|
|
109
|
+
- [Hermes Grix Runtime](../shared/references/hermes-grix-config.md)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const sharedCli = path.resolve(scriptDir, "../../shared/cli/grix-hermes.mjs");
|
|
9
|
+
const result = spawnSync(process.execPath, [sharedCli, "admin", ...process.argv.slice(2)], {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
});
|
|
12
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract bind-local fields from JSON and forward them to bind_local.py."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
15
|
+
if args.from_file:
|
|
16
|
+
return json.loads(Path(args.from_file).read_text(encoding="utf-8"))
|
|
17
|
+
raw = sys.stdin.read().strip()
|
|
18
|
+
if not raw:
|
|
19
|
+
raise RuntimeError("No JSON input provided.")
|
|
20
|
+
return json.loads(raw)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clean_text(value: Any) -> str:
|
|
24
|
+
return str(value or "").strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def as_record(value: Any) -> dict[str, Any]:
|
|
28
|
+
return value if isinstance(value, dict) else {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_bind_fields(payload: dict[str, Any]) -> dict[str, str]:
|
|
32
|
+
handoff = as_record(payload.get("handoff"))
|
|
33
|
+
bind_local = as_record(handoff.get("bind_local"))
|
|
34
|
+
if bind_local:
|
|
35
|
+
return {
|
|
36
|
+
"agent_name": clean_text(bind_local.get("agent_name")),
|
|
37
|
+
"agent_id": clean_text(bind_local.get("agent_id")),
|
|
38
|
+
"api_endpoint": clean_text(bind_local.get("api_endpoint")),
|
|
39
|
+
"api_key": clean_text(bind_local.get("api_key")),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
created_agent = as_record(payload.get("createdAgent"))
|
|
43
|
+
if created_agent:
|
|
44
|
+
return {
|
|
45
|
+
"agent_name": clean_text(created_agent.get("agent_name") or created_agent.get("name")),
|
|
46
|
+
"agent_id": clean_text(created_agent.get("id") or created_agent.get("agent_id")),
|
|
47
|
+
"api_endpoint": clean_text(created_agent.get("api_endpoint") or payload.get("api_endpoint")),
|
|
48
|
+
"api_key": clean_text(created_agent.get("api_key") or payload.get("api_key")),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"agent_name": clean_text(payload.get("agent_name") or payload.get("name")),
|
|
53
|
+
"agent_id": clean_text(payload.get("agent_id") or payload.get("id")),
|
|
54
|
+
"api_endpoint": clean_text(payload.get("api_endpoint")),
|
|
55
|
+
"api_key": clean_text(payload.get("api_key")),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
parser = argparse.ArgumentParser(description="Read agent JSON and forward it to bind_local.py.")
|
|
61
|
+
parser.add_argument("--from-file", default="")
|
|
62
|
+
parser.add_argument("--model", default="")
|
|
63
|
+
parser.add_argument("--openclaw", default="openclaw")
|
|
64
|
+
parser.add_argument("--openclaw-home", default="")
|
|
65
|
+
parser.add_argument("--skip-current", action="store_true")
|
|
66
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
67
|
+
parser.add_argument("--json", action="store_true")
|
|
68
|
+
args = parser.parse_args()
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
payload = load_payload(args)
|
|
72
|
+
bind_fields = extract_bind_fields(payload)
|
|
73
|
+
missing = [key for key, value in bind_fields.items() if not value]
|
|
74
|
+
if missing:
|
|
75
|
+
raise RuntimeError(f"Missing bind-local fields: {', '.join(missing)}")
|
|
76
|
+
bind_script = Path(__file__).with_name("bind_local.py")
|
|
77
|
+
cmd = [
|
|
78
|
+
sys.executable,
|
|
79
|
+
str(bind_script),
|
|
80
|
+
"--agent-name",
|
|
81
|
+
bind_fields["agent_name"],
|
|
82
|
+
"--agent-id",
|
|
83
|
+
bind_fields["agent_id"],
|
|
84
|
+
"--api-endpoint",
|
|
85
|
+
bind_fields["api_endpoint"],
|
|
86
|
+
"--api-key",
|
|
87
|
+
bind_fields["api_key"],
|
|
88
|
+
"--openclaw",
|
|
89
|
+
args.openclaw,
|
|
90
|
+
]
|
|
91
|
+
if args.model:
|
|
92
|
+
cmd.extend(["--model", args.model])
|
|
93
|
+
if args.openclaw_home:
|
|
94
|
+
cmd.extend(["--openclaw-home", args.openclaw_home])
|
|
95
|
+
if args.skip_current:
|
|
96
|
+
cmd.append("--skip-current")
|
|
97
|
+
if args.dry_run:
|
|
98
|
+
cmd.append("--dry-run")
|
|
99
|
+
if args.json:
|
|
100
|
+
cmd.append("--json")
|
|
101
|
+
|
|
102
|
+
result = subprocess.run(cmd, text=True, capture_output=True)
|
|
103
|
+
if result.returncode != 0:
|
|
104
|
+
raise RuntimeError((result.stderr or result.stdout or "").strip())
|
|
105
|
+
|
|
106
|
+
if result.stdout:
|
|
107
|
+
sys.stdout.write(result.stdout)
|
|
108
|
+
return 0
|
|
109
|
+
except Exception as exc: # noqa: BLE001
|
|
110
|
+
if args.json:
|
|
111
|
+
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
112
|
+
else:
|
|
113
|
+
print(str(exc), file=sys.stderr)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bind one remote Grix API agent into local OpenClaw config."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MINIMAL_PERSONA_FILES = {
|
|
16
|
+
"IDENTITY.md": "# Identity\n\nThis agent is managed by grix-hermes.\n",
|
|
17
|
+
"SOUL.md": "# Soul\n\nRespond clearly and keep the workflow moving.\n",
|
|
18
|
+
"AGENTS.md": "# Agents\n\nUse the current workspace and configured tools.\n",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_command(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
23
|
+
result = subprocess.run(cmd, text=True, capture_output=True)
|
|
24
|
+
if check and result.returncode != 0:
|
|
25
|
+
stderr = (result.stderr or "").strip()
|
|
26
|
+
stdout = (result.stdout or "").strip()
|
|
27
|
+
raise RuntimeError(stderr or stdout or f"command failed: {' '.join(cmd)}")
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def safe_config_get(openclaw_cmd: str, path: str, default: Any) -> Any:
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
[openclaw_cmd, "config", "get", path, "--json"],
|
|
34
|
+
text=True,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
)
|
|
37
|
+
if result.returncode != 0:
|
|
38
|
+
return default
|
|
39
|
+
text = (result.stdout or "").strip()
|
|
40
|
+
if not text:
|
|
41
|
+
return default
|
|
42
|
+
try:
|
|
43
|
+
return json.loads(text)
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
return default
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ensure_dir(path: Path) -> None:
|
|
49
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ensure_minimal_persona(workspace: Path) -> list[str]:
|
|
53
|
+
ensure_dir(workspace)
|
|
54
|
+
created: list[str] = []
|
|
55
|
+
for file_name, content in MINIMAL_PERSONA_FILES.items():
|
|
56
|
+
file_path = workspace / file_name
|
|
57
|
+
if file_path.exists():
|
|
58
|
+
continue
|
|
59
|
+
file_path.write_text(content, encoding="utf-8")
|
|
60
|
+
created.append(str(file_path))
|
|
61
|
+
return created
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def merge_tool_allowlist(current: Any) -> list[str]:
|
|
65
|
+
existing: list[str] = []
|
|
66
|
+
if isinstance(current, list):
|
|
67
|
+
for item in current:
|
|
68
|
+
text = str(item or "").strip()
|
|
69
|
+
if text:
|
|
70
|
+
existing.append(text)
|
|
71
|
+
if "message" not in existing:
|
|
72
|
+
existing.append("message")
|
|
73
|
+
return existing
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_model(args: argparse.Namespace, agents_list: list[dict[str, Any]], openclaw_cmd: str, skip_current: bool) -> str:
|
|
77
|
+
explicit = str(args.model or "").strip()
|
|
78
|
+
if explicit:
|
|
79
|
+
return explicit
|
|
80
|
+
for entry in agents_list:
|
|
81
|
+
if str(entry.get("id") or "").strip() == args.agent_name:
|
|
82
|
+
model = str(entry.get("model") or "").strip()
|
|
83
|
+
if model:
|
|
84
|
+
return model
|
|
85
|
+
if skip_current:
|
|
86
|
+
return ""
|
|
87
|
+
defaults = safe_config_get(openclaw_cmd, "agents.defaults.model.primary", "")
|
|
88
|
+
return str(defaults or "").strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_plan(args: argparse.Namespace) -> dict[str, Any]:
|
|
92
|
+
openclaw_cmd = args.openclaw
|
|
93
|
+
skip_current = bool(args.skip_current)
|
|
94
|
+
current_accounts = {} if skip_current else safe_config_get(openclaw_cmd, "channels.grix.accounts", {})
|
|
95
|
+
current_agents = [] if skip_current else safe_config_get(openclaw_cmd, "agents.list", [])
|
|
96
|
+
current_profile = "coding" if skip_current else safe_config_get(openclaw_cmd, "tools.profile", "coding")
|
|
97
|
+
current_allow = [] if skip_current else safe_config_get(openclaw_cmd, "tools.alsoAllow", [])
|
|
98
|
+
current_visibility = "agent" if skip_current else safe_config_get(openclaw_cmd, "tools.sessions.visibility", "agent")
|
|
99
|
+
current_grix_enabled = True if skip_current else safe_config_get(openclaw_cmd, "channels.grix.enabled", True)
|
|
100
|
+
|
|
101
|
+
openclaw_root = Path(os.path.expanduser(args.openclaw_home or "~/.openclaw"))
|
|
102
|
+
workspace = openclaw_root / f"workspace-{args.agent_name}"
|
|
103
|
+
agent_dir = openclaw_root / "agents" / args.agent_name / "agent"
|
|
104
|
+
model = resolve_model(args, current_agents if isinstance(current_agents, list) else [], openclaw_cmd, skip_current)
|
|
105
|
+
if not model:
|
|
106
|
+
raise RuntimeError("Missing model. Pass --model or ensure agents.defaults.model.primary exists.")
|
|
107
|
+
|
|
108
|
+
account_json = {
|
|
109
|
+
"name": args.agent_name,
|
|
110
|
+
"enabled": True,
|
|
111
|
+
"apiKey": args.api_key,
|
|
112
|
+
"wsUrl": args.api_endpoint,
|
|
113
|
+
"agentId": args.agent_id,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
next_agents = []
|
|
117
|
+
replaced = False
|
|
118
|
+
for entry in current_agents if isinstance(current_agents, list) else []:
|
|
119
|
+
if str(entry.get("id") or "").strip() != args.agent_name:
|
|
120
|
+
next_agents.append(entry)
|
|
121
|
+
continue
|
|
122
|
+
replaced = True
|
|
123
|
+
updated = dict(entry)
|
|
124
|
+
updated["id"] = args.agent_name
|
|
125
|
+
updated["name"] = args.agent_name
|
|
126
|
+
updated["workspace"] = str(workspace)
|
|
127
|
+
updated["agentDir"] = str(agent_dir)
|
|
128
|
+
updated["model"] = model
|
|
129
|
+
next_agents.append(updated)
|
|
130
|
+
if not replaced:
|
|
131
|
+
next_agents.append(
|
|
132
|
+
{
|
|
133
|
+
"id": args.agent_name,
|
|
134
|
+
"name": args.agent_name,
|
|
135
|
+
"workspace": str(workspace),
|
|
136
|
+
"agentDir": str(agent_dir),
|
|
137
|
+
"model": model,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
commands: list[list[str]] = [
|
|
142
|
+
[openclaw_cmd, "config", "set", f"channels.grix.accounts.{args.agent_name}", json.dumps(account_json, ensure_ascii=False), "--strict-json"],
|
|
143
|
+
[openclaw_cmd, "config", "set", "agents.list", json.dumps(next_agents, ensure_ascii=False), "--strict-json"],
|
|
144
|
+
[openclaw_cmd, "agents", "bind", "--agent", args.agent_name, "--bind", f"grix:{args.agent_name}", "--json"],
|
|
145
|
+
[openclaw_cmd, "config", "set", "tools.profile", json.dumps(str(current_profile or "coding")), "--strict-json"],
|
|
146
|
+
[openclaw_cmd, "config", "set", "tools.alsoAllow", json.dumps(merge_tool_allowlist(current_allow), ensure_ascii=False), "--strict-json"],
|
|
147
|
+
[openclaw_cmd, "config", "set", "tools.sessions.visibility", json.dumps(str(current_visibility or "agent")), "--strict-json"],
|
|
148
|
+
]
|
|
149
|
+
if current_grix_enabled is False:
|
|
150
|
+
commands.append([openclaw_cmd, "config", "set", "channels.grix.enabled", "true", "--strict-json"])
|
|
151
|
+
commands.extend(
|
|
152
|
+
[
|
|
153
|
+
[openclaw_cmd, "config", "validate"],
|
|
154
|
+
[openclaw_cmd, "config", "get", f"channels.grix.accounts.{args.agent_name}", "--json"],
|
|
155
|
+
[openclaw_cmd, "config", "get", "agents.list", "--json"],
|
|
156
|
+
[openclaw_cmd, "agents", "bindings", "--agent", args.agent_name, "--json"],
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"agent_name": args.agent_name,
|
|
162
|
+
"agent_id": args.agent_id,
|
|
163
|
+
"workspace": str(workspace),
|
|
164
|
+
"agent_dir": str(agent_dir),
|
|
165
|
+
"model": model,
|
|
166
|
+
"account": account_json,
|
|
167
|
+
"agents_list": next_agents,
|
|
168
|
+
"commands": commands,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main() -> int:
|
|
173
|
+
parser = argparse.ArgumentParser(description="Bind one Grix API agent into local OpenClaw config.")
|
|
174
|
+
parser.add_argument("--agent-name", required=True)
|
|
175
|
+
parser.add_argument("--agent-id", required=True)
|
|
176
|
+
parser.add_argument("--api-endpoint", required=True)
|
|
177
|
+
parser.add_argument("--api-key", required=True)
|
|
178
|
+
parser.add_argument("--model", default="")
|
|
179
|
+
parser.add_argument("--openclaw", default="openclaw")
|
|
180
|
+
parser.add_argument("--openclaw-home", default="")
|
|
181
|
+
parser.add_argument("--skip-current", action="store_true", help="Do not read current OpenClaw config before building the plan.")
|
|
182
|
+
parser.add_argument("--dry-run", action="store_true", help="Only print the planned result and commands.")
|
|
183
|
+
parser.add_argument("--json", action="store_true", help="Emit JSON summary.")
|
|
184
|
+
args = parser.parse_args()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
plan = build_plan(args)
|
|
188
|
+
created_files: list[str] = []
|
|
189
|
+
command_results: list[dict[str, Any]] = []
|
|
190
|
+
if not args.dry_run:
|
|
191
|
+
created_files = ensure_minimal_persona(Path(plan["workspace"]))
|
|
192
|
+
ensure_dir(Path(plan["agent_dir"]))
|
|
193
|
+
for cmd in plan["commands"]:
|
|
194
|
+
result = run_command(cmd)
|
|
195
|
+
command_results.append(
|
|
196
|
+
{
|
|
197
|
+
"cmd": cmd,
|
|
198
|
+
"stdout": (result.stdout or "").strip(),
|
|
199
|
+
"stderr": (result.stderr or "").strip(),
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
payload = {
|
|
203
|
+
"ok": True,
|
|
204
|
+
"dry_run": bool(args.dry_run),
|
|
205
|
+
"created_files": created_files,
|
|
206
|
+
"command_results": command_results,
|
|
207
|
+
**plan,
|
|
208
|
+
}
|
|
209
|
+
if args.json:
|
|
210
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
211
|
+
else:
|
|
212
|
+
print(f"agent={plan['agent_name']} workspace={plan['workspace']} dry_run={args.dry_run}")
|
|
213
|
+
for cmd in plan["commands"]:
|
|
214
|
+
print("$ " + " ".join(cmd))
|
|
215
|
+
return 0
|
|
216
|
+
except Exception as exc: # noqa: BLE001
|
|
217
|
+
payload = {"ok": False, "error": str(exc)}
|
|
218
|
+
if args.json:
|
|
219
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
220
|
+
else:
|
|
221
|
+
print(str(exc), file=sys.stderr)
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
raise SystemExit(main())
|