@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
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://grix.dhf.pub"
|
|
15
|
+
DEFAULT_TIMEOUT_SECONDS = 15
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_default_base_url() -> str:
|
|
19
|
+
return (os.environ.get("GRIX_WEB_BASE_URL", "") or "").strip() or DEFAULT_BASE_URL
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def derive_portal_url(raw_base_url: str) -> str:
|
|
23
|
+
base = (raw_base_url or "").strip() or resolve_default_base_url()
|
|
24
|
+
parsed = urllib.parse.urlparse(base)
|
|
25
|
+
if not parsed.scheme or not parsed.netloc:
|
|
26
|
+
raise ValueError(f"Invalid base URL: {base}")
|
|
27
|
+
|
|
28
|
+
path = parsed.path.rstrip("/")
|
|
29
|
+
if path.endswith("/v1"):
|
|
30
|
+
path = path[: -len("/v1")]
|
|
31
|
+
|
|
32
|
+
normalized = parsed._replace(path=path or "/", params="", query="", fragment="")
|
|
33
|
+
return urllib.parse.urlunparse(normalized).rstrip("/") + "/"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GrixAuthError(RuntimeError):
|
|
37
|
+
def __init__(self, message, status=0, code=-1, payload=None):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status = status
|
|
40
|
+
self.code = code
|
|
41
|
+
self.payload = payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_base_url(raw_base_url: str) -> str:
|
|
45
|
+
base = (raw_base_url or "").strip() or resolve_default_base_url()
|
|
46
|
+
parsed = urllib.parse.urlparse(base)
|
|
47
|
+
if not parsed.scheme or not parsed.netloc:
|
|
48
|
+
raise ValueError(f"Invalid base URL: {base}")
|
|
49
|
+
|
|
50
|
+
path = parsed.path.rstrip("/")
|
|
51
|
+
if not path:
|
|
52
|
+
path = "/v1"
|
|
53
|
+
elif not path.endswith("/v1"):
|
|
54
|
+
path = f"{path}/v1"
|
|
55
|
+
|
|
56
|
+
normalized = parsed._replace(path=path, params="", query="", fragment="")
|
|
57
|
+
return urllib.parse.urlunparse(normalized).rstrip("/")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def request_json(method: str, path: str, base_url: str, body=None, headers=None):
|
|
61
|
+
api_base_url = normalize_base_url(base_url)
|
|
62
|
+
url = f"{api_base_url}{path if path.startswith('/') else '/' + path}"
|
|
63
|
+
data = None
|
|
64
|
+
final_headers = dict(headers or {})
|
|
65
|
+
if body is not None:
|
|
66
|
+
data = json.dumps(body).encode("utf-8")
|
|
67
|
+
final_headers["Content-Type"] = "application/json"
|
|
68
|
+
|
|
69
|
+
req = urllib.request.Request(url=url, data=data, headers=final_headers, method=method)
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT_SECONDS) as resp:
|
|
72
|
+
status = getattr(resp, "status", 200)
|
|
73
|
+
raw = resp.read().decode("utf-8")
|
|
74
|
+
except urllib.error.HTTPError as exc:
|
|
75
|
+
status = exc.code
|
|
76
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
77
|
+
except urllib.error.URLError as exc:
|
|
78
|
+
raise GrixAuthError(f"network error: {exc.reason}") from exc
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
payload = json.loads(raw)
|
|
82
|
+
except json.JSONDecodeError as exc:
|
|
83
|
+
raise GrixAuthError(f"invalid json response: {raw[:256]}", status=status) from exc
|
|
84
|
+
|
|
85
|
+
code = int(payload.get("code", -1))
|
|
86
|
+
msg = str(payload.get("msg", "")).strip() or "unknown error"
|
|
87
|
+
if status >= 400 or code != 0:
|
|
88
|
+
raise GrixAuthError(msg, status=status, code=code, payload=payload)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"api_base_url": api_base_url,
|
|
92
|
+
"status": status,
|
|
93
|
+
"data": payload.get("data"),
|
|
94
|
+
"payload": payload,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def maybe_write_captcha_image(b64s: str):
|
|
99
|
+
text = (b64s or "").strip()
|
|
100
|
+
if not text.startswith("data:image/"):
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
marker = ";base64,"
|
|
104
|
+
idx = text.find(marker)
|
|
105
|
+
if idx < 0:
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
encoded = text[idx + len(marker) :]
|
|
109
|
+
try:
|
|
110
|
+
content = base64.b64decode(encoded)
|
|
111
|
+
except Exception:
|
|
112
|
+
return ""
|
|
113
|
+
|
|
114
|
+
fd, path = tempfile.mkstemp(prefix="grix-captcha-", suffix=".png")
|
|
115
|
+
try:
|
|
116
|
+
with os.fdopen(fd, "wb") as handle:
|
|
117
|
+
handle.write(content)
|
|
118
|
+
except Exception:
|
|
119
|
+
try:
|
|
120
|
+
os.unlink(path)
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
return ""
|
|
124
|
+
return path
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def print_json(payload):
|
|
128
|
+
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
|
|
129
|
+
sys.stdout.write("\n")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_auth_result(action: str, result: dict, base_url: str):
|
|
133
|
+
data = result.get("data") or {}
|
|
134
|
+
user = data.get("user") or {}
|
|
135
|
+
return {
|
|
136
|
+
"ok": True,
|
|
137
|
+
"action": action,
|
|
138
|
+
"api_base_url": result["api_base_url"],
|
|
139
|
+
"access_token": data.get("access_token", ""),
|
|
140
|
+
"refresh_token": data.get("refresh_token", ""),
|
|
141
|
+
"expires_in": data.get("expires_in", 0),
|
|
142
|
+
"user_id": user.get("id", ""),
|
|
143
|
+
"portal_url": derive_portal_url(base_url),
|
|
144
|
+
"data": data,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_agent_result(action: str, result: dict):
|
|
149
|
+
data = result.get("data") or {}
|
|
150
|
+
agent_id = str(data.get("id", "")).strip()
|
|
151
|
+
api_endpoint = str(data.get("api_endpoint", "")).strip()
|
|
152
|
+
api_key = str(data.get("api_key", "")).strip()
|
|
153
|
+
agent_name = str(data.get("agent_name", "")).strip()
|
|
154
|
+
bind_local_payload = {
|
|
155
|
+
"agent_name": agent_name,
|
|
156
|
+
"agent_id": agent_id,
|
|
157
|
+
"api_endpoint": api_endpoint,
|
|
158
|
+
"api_key": api_key,
|
|
159
|
+
}
|
|
160
|
+
handoff_task = "\n".join(
|
|
161
|
+
[
|
|
162
|
+
"bind-local",
|
|
163
|
+
f"agent_name={agent_name}",
|
|
164
|
+
f"agent_id={agent_id}",
|
|
165
|
+
f"api_endpoint={api_endpoint}",
|
|
166
|
+
f"api_key={api_key}",
|
|
167
|
+
"do_not_create_remote_agent=true",
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"ok": True,
|
|
173
|
+
"action": action,
|
|
174
|
+
"api_base_url": result["api_base_url"],
|
|
175
|
+
"agent_id": agent_id,
|
|
176
|
+
"agent_name": agent_name,
|
|
177
|
+
"provider_type": data.get("provider_type", 0),
|
|
178
|
+
"api_endpoint": api_endpoint,
|
|
179
|
+
"api_key": api_key,
|
|
180
|
+
"api_key_hint": data.get("api_key_hint", ""),
|
|
181
|
+
"session_id": data.get("session_id", ""),
|
|
182
|
+
"handoff": {
|
|
183
|
+
"target_tool": "grix_admin",
|
|
184
|
+
"task": handoff_task,
|
|
185
|
+
"bind_local": bind_local_payload,
|
|
186
|
+
},
|
|
187
|
+
"data": data,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def login_with_credentials(base_url: str, account: str, password: str, device_id: str, platform: str):
|
|
192
|
+
result = request_json(
|
|
193
|
+
"POST",
|
|
194
|
+
"/auth/login",
|
|
195
|
+
base_url,
|
|
196
|
+
body={
|
|
197
|
+
"account": account,
|
|
198
|
+
"password": password,
|
|
199
|
+
"device_id": device_id,
|
|
200
|
+
"platform": platform,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
return build_auth_result("login", result, base_url)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def create_api_agent(base_url: str, access_token: str, agent_name: str, avatar_url: str):
|
|
207
|
+
request_body = {
|
|
208
|
+
"agent_name": agent_name.strip(),
|
|
209
|
+
"provider_type": 3,
|
|
210
|
+
"is_main": True,
|
|
211
|
+
}
|
|
212
|
+
normalized_avatar_url = (avatar_url or "").strip()
|
|
213
|
+
if normalized_avatar_url:
|
|
214
|
+
request_body["avatar_url"] = normalized_avatar_url
|
|
215
|
+
|
|
216
|
+
result = request_json(
|
|
217
|
+
"POST",
|
|
218
|
+
"/agents/create",
|
|
219
|
+
base_url,
|
|
220
|
+
body=request_body,
|
|
221
|
+
headers={
|
|
222
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
return build_agent_result("create-api-agent", result)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def list_agents(base_url: str, access_token: str):
|
|
229
|
+
result = request_json(
|
|
230
|
+
"GET",
|
|
231
|
+
"/agents/list",
|
|
232
|
+
base_url,
|
|
233
|
+
headers={
|
|
234
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
data = result.get("data") or {}
|
|
238
|
+
items = data.get("list") or []
|
|
239
|
+
if not isinstance(items, list):
|
|
240
|
+
items = []
|
|
241
|
+
return items
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def rotate_api_agent_key(base_url: str, access_token: str, agent_id: str):
|
|
245
|
+
result = request_json(
|
|
246
|
+
"POST",
|
|
247
|
+
f"/agents/{str(agent_id).strip()}/api/key/rotate",
|
|
248
|
+
base_url,
|
|
249
|
+
body={},
|
|
250
|
+
headers={
|
|
251
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
return build_agent_result("rotate-api-agent-key", result)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def find_existing_api_agent(agents, agent_name: str):
|
|
258
|
+
normalized_name = (agent_name or "").strip()
|
|
259
|
+
if not normalized_name:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
for item in agents:
|
|
263
|
+
if not isinstance(item, dict):
|
|
264
|
+
continue
|
|
265
|
+
if str(item.get("agent_name", "")).strip() != normalized_name:
|
|
266
|
+
continue
|
|
267
|
+
if int(item.get("provider_type", 0) or 0) != 3:
|
|
268
|
+
continue
|
|
269
|
+
if int(item.get("status", 0) or 0) == 3:
|
|
270
|
+
continue
|
|
271
|
+
return item
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def create_or_reuse_api_agent(
|
|
276
|
+
base_url: str,
|
|
277
|
+
access_token: str,
|
|
278
|
+
agent_name: str,
|
|
279
|
+
avatar_url: str,
|
|
280
|
+
prefer_existing: bool,
|
|
281
|
+
rotate_on_reuse: bool,
|
|
282
|
+
):
|
|
283
|
+
if prefer_existing:
|
|
284
|
+
agents = list_agents(base_url, access_token)
|
|
285
|
+
existing = find_existing_api_agent(agents, agent_name)
|
|
286
|
+
if existing is not None:
|
|
287
|
+
if not rotate_on_reuse:
|
|
288
|
+
raise GrixAuthError(
|
|
289
|
+
"existing provider_type=3 agent found but rotate-on-reuse is disabled; cannot obtain api_key safely",
|
|
290
|
+
payload={"existing_agent": existing},
|
|
291
|
+
)
|
|
292
|
+
rotated = rotate_api_agent_key(base_url, access_token, str(existing.get("id", "")).strip())
|
|
293
|
+
rotated["source"] = "reused_existing_agent_with_rotated_key"
|
|
294
|
+
rotated["existing_agent"] = existing
|
|
295
|
+
return rotated
|
|
296
|
+
|
|
297
|
+
created = create_api_agent(base_url, access_token, agent_name, avatar_url)
|
|
298
|
+
created["source"] = "created_new_agent"
|
|
299
|
+
return created
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def default_device_id(platform: str) -> str:
|
|
303
|
+
normalized_platform = (platform or "").strip() or "web"
|
|
304
|
+
return f"{normalized_platform}_{uuid.uuid4()}"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def handle_fetch_captcha(args):
|
|
308
|
+
result = request_json("GET", "/auth/captcha", args.base_url)
|
|
309
|
+
data = result.get("data") or {}
|
|
310
|
+
image_path = maybe_write_captcha_image(str(data.get("b64s", "")))
|
|
311
|
+
payload = {
|
|
312
|
+
"ok": True,
|
|
313
|
+
"action": "fetch-captcha",
|
|
314
|
+
"api_base_url": result["api_base_url"],
|
|
315
|
+
"captcha_id": data.get("captcha_id", ""),
|
|
316
|
+
"b64s": data.get("b64s", ""),
|
|
317
|
+
}
|
|
318
|
+
if image_path:
|
|
319
|
+
payload["captcha_image_path"] = image_path
|
|
320
|
+
print_json(payload)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def handle_send_email_code(args):
|
|
324
|
+
scene = args.scene.strip()
|
|
325
|
+
payload = {
|
|
326
|
+
"email": args.email.strip(),
|
|
327
|
+
"scene": scene,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
captcha_id = (args.captcha_id or "").strip()
|
|
331
|
+
captcha_value = (args.captcha_value or "").strip()
|
|
332
|
+
if scene in {"reset", "change_password"}:
|
|
333
|
+
if not captcha_id or not captcha_value:
|
|
334
|
+
raise GrixAuthError("captcha_id and captcha_value are required for reset/change_password")
|
|
335
|
+
if captcha_id:
|
|
336
|
+
payload["captcha_id"] = captcha_id
|
|
337
|
+
if captcha_value:
|
|
338
|
+
payload["captcha_value"] = captcha_value
|
|
339
|
+
|
|
340
|
+
result = request_json(
|
|
341
|
+
"POST",
|
|
342
|
+
"/auth/send-code",
|
|
343
|
+
args.base_url,
|
|
344
|
+
body=payload,
|
|
345
|
+
)
|
|
346
|
+
print_json(
|
|
347
|
+
{
|
|
348
|
+
"ok": True,
|
|
349
|
+
"action": "send-email-code",
|
|
350
|
+
"api_base_url": result["api_base_url"],
|
|
351
|
+
"data": result.get("data"),
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def handle_register(args):
|
|
357
|
+
platform = (args.platform or "").strip() or "web"
|
|
358
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
359
|
+
result = request_json(
|
|
360
|
+
"POST",
|
|
361
|
+
"/auth/register",
|
|
362
|
+
args.base_url,
|
|
363
|
+
body={
|
|
364
|
+
"email": args.email.strip(),
|
|
365
|
+
"password": args.password.strip(),
|
|
366
|
+
"email_code": args.email_code.strip(),
|
|
367
|
+
"device_id": device_id,
|
|
368
|
+
"platform": platform,
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
print_json(build_auth_result("register", result, args.base_url))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def handle_login(args):
|
|
375
|
+
account = (args.email or args.account or "").strip()
|
|
376
|
+
if not account:
|
|
377
|
+
raise GrixAuthError("either --email or --account is required")
|
|
378
|
+
|
|
379
|
+
platform = (args.platform or "").strip() or "web"
|
|
380
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
381
|
+
print_json(
|
|
382
|
+
login_with_credentials(
|
|
383
|
+
args.base_url,
|
|
384
|
+
account,
|
|
385
|
+
args.password.strip(),
|
|
386
|
+
device_id,
|
|
387
|
+
platform,
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def handle_create_api_agent(args):
|
|
393
|
+
print_json(
|
|
394
|
+
create_or_reuse_api_agent(
|
|
395
|
+
args.base_url,
|
|
396
|
+
args.access_token.strip(),
|
|
397
|
+
args.agent_name.strip(),
|
|
398
|
+
args.avatar_url,
|
|
399
|
+
not bool(args.no_reuse_existing_agent),
|
|
400
|
+
not bool(args.no_rotate_key_on_reuse),
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def build_parser():
|
|
406
|
+
parser = argparse.ArgumentParser(description="Grix public auth API helper")
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"--base-url",
|
|
409
|
+
default=resolve_default_base_url(),
|
|
410
|
+
help="Grix web base URL (defaults to GRIX_WEB_BASE_URL or https://grix.dhf.pub)",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
414
|
+
|
|
415
|
+
fetch_captcha = subparsers.add_parser("fetch-captcha", help="Fetch captcha image")
|
|
416
|
+
fetch_captcha.set_defaults(handler=handle_fetch_captcha)
|
|
417
|
+
|
|
418
|
+
send_email_code = subparsers.add_parser("send-email-code", help="Send email verification code")
|
|
419
|
+
send_email_code.add_argument("--email", required=True)
|
|
420
|
+
send_email_code.add_argument("--scene", required=True, choices=["register", "reset", "change_password"])
|
|
421
|
+
send_email_code.add_argument("--captcha-id", default="")
|
|
422
|
+
send_email_code.add_argument("--captcha-value", default="")
|
|
423
|
+
send_email_code.set_defaults(handler=handle_send_email_code)
|
|
424
|
+
|
|
425
|
+
register = subparsers.add_parser("register", help="Register by email verification code")
|
|
426
|
+
register.add_argument("--email", required=True)
|
|
427
|
+
register.add_argument("--password", required=True)
|
|
428
|
+
register.add_argument("--email-code", required=True)
|
|
429
|
+
register.add_argument("--device-id", default="")
|
|
430
|
+
register.add_argument("--platform", default="web")
|
|
431
|
+
register.set_defaults(handler=handle_register)
|
|
432
|
+
|
|
433
|
+
login = subparsers.add_parser("login", help="Login and obtain tokens")
|
|
434
|
+
login_identity = login.add_mutually_exclusive_group(required=True)
|
|
435
|
+
login_identity.add_argument("--account")
|
|
436
|
+
login_identity.add_argument("--email")
|
|
437
|
+
login.add_argument("--password", required=True)
|
|
438
|
+
login.add_argument("--device-id", default="")
|
|
439
|
+
login.add_argument("--platform", default="web")
|
|
440
|
+
login.set_defaults(handler=handle_login)
|
|
441
|
+
|
|
442
|
+
create_api_agent_parser = subparsers.add_parser(
|
|
443
|
+
"create-api-agent",
|
|
444
|
+
help="Create a provider_type=3 API agent with a user access token",
|
|
445
|
+
)
|
|
446
|
+
create_api_agent_parser.add_argument("--access-token", required=True)
|
|
447
|
+
create_api_agent_parser.add_argument("--agent-name", required=True)
|
|
448
|
+
create_api_agent_parser.add_argument("--avatar-url", default="")
|
|
449
|
+
create_api_agent_parser.add_argument("--no-reuse-existing-agent", action="store_true")
|
|
450
|
+
create_api_agent_parser.add_argument("--no-rotate-key-on-reuse", action="store_true")
|
|
451
|
+
create_api_agent_parser.set_defaults(handler=handle_create_api_agent)
|
|
452
|
+
|
|
453
|
+
return parser
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def main():
|
|
457
|
+
parser = build_parser()
|
|
458
|
+
args = parser.parse_args()
|
|
459
|
+
try:
|
|
460
|
+
args.handler(args)
|
|
461
|
+
except GrixAuthError as exc:
|
|
462
|
+
print_json(
|
|
463
|
+
{
|
|
464
|
+
"ok": False,
|
|
465
|
+
"action": args.action,
|
|
466
|
+
"status": exc.status,
|
|
467
|
+
"code": exc.code,
|
|
468
|
+
"error": str(exc),
|
|
469
|
+
"payload": exc.payload,
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
raise SystemExit(1)
|
|
473
|
+
except Exception as exc:
|
|
474
|
+
print_json(
|
|
475
|
+
{
|
|
476
|
+
"ok": False,
|
|
477
|
+
"action": args.action,
|
|
478
|
+
"status": 0,
|
|
479
|
+
"code": -1,
|
|
480
|
+
"error": str(exc),
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
raise SystemExit(1)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grix-update
|
|
3
|
+
description: 需要检查或执行 OpenClaw 的 Grix 插件升级时使用。适用于手动维护或 cron 维护场景,只通过 `openclaw` 官方 CLI 完成检查、升级、校验、可选重启和健康检查。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Grix Update
|
|
7
|
+
|
|
8
|
+
这个技能只做 OpenClaw 运维。
|
|
9
|
+
|
|
10
|
+
## 输入
|
|
11
|
+
|
|
12
|
+
建议把输入理解成:
|
|
13
|
+
|
|
14
|
+
- `mode`: `check-only` / `apply-update` / `check-and-apply`
|
|
15
|
+
- `plugin_id`: 默认 `grix`
|
|
16
|
+
- `allow_restart`: 默认 `true`
|
|
17
|
+
|
|
18
|
+
## 执行顺序
|
|
19
|
+
|
|
20
|
+
优先使用 helper:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python3 scripts/grix_update.py --mode check-and-apply --plugin-id grix --allow-restart true --json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
它内部会按当前 OpenClaw CLI 真实命令执行:
|
|
27
|
+
|
|
28
|
+
1. `openclaw plugins inspect <plugin_id> --json`
|
|
29
|
+
2. `openclaw plugins update <plugin_id> --dry-run`
|
|
30
|
+
3. 按模式决定是否真正升级
|
|
31
|
+
4. 升级后执行:
|
|
32
|
+
- `openclaw plugins doctor`
|
|
33
|
+
- `openclaw gateway restart`(仅在允许时)
|
|
34
|
+
- `openclaw health --json`
|
|
35
|
+
|
|
36
|
+
## Guardrails
|
|
37
|
+
|
|
38
|
+
- 不要改用非官方脚本
|
|
39
|
+
- 如果 `allow_restart=false`,明确告诉上层运行态可能还是旧版本
|
|
40
|
+
- 如果没有明确通知目标,不要自行猜消息发送目标
|
|
41
|
+
|
|
42
|
+
## 推荐 cron 接法
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
Use the grix-update skill with {"mode":"check-and-apply","plugin_id":"grix","notify_on":"never","allow_restart":true}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 参考
|
|
49
|
+
|
|
50
|
+
- [Cron Setup](references/cron-setup.md)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run OpenClaw Grix plugin update flow with a stable CLI surface."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_command(cmd: list[str], *, check: bool = True) -> dict[str, Any]:
|
|
14
|
+
result = subprocess.run(cmd, text=True, capture_output=True)
|
|
15
|
+
payload = {
|
|
16
|
+
"cmd": cmd,
|
|
17
|
+
"code": result.returncode,
|
|
18
|
+
"stdout": (result.stdout or "").strip(),
|
|
19
|
+
"stderr": (result.stderr or "").strip(),
|
|
20
|
+
}
|
|
21
|
+
if check and result.returncode != 0:
|
|
22
|
+
raise RuntimeError(payload["stderr"] or payload["stdout"] or f"command failed: {' '.join(cmd)}")
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_plan(args: argparse.Namespace) -> list[list[str]]:
|
|
27
|
+
openclaw_cmd = args.openclaw
|
|
28
|
+
plugin_id = args.plugin_id
|
|
29
|
+
inspect_cmd = [openclaw_cmd, "plugins", "inspect", plugin_id, "--json"]
|
|
30
|
+
update_probe_cmd = [openclaw_cmd, "plugins", "update", plugin_id, "--dry-run"]
|
|
31
|
+
apply_cmd = [openclaw_cmd, "plugins", "update", plugin_id]
|
|
32
|
+
doctor_cmd = [openclaw_cmd, "plugins", "doctor"]
|
|
33
|
+
restart_cmd = [openclaw_cmd, "gateway", "restart"]
|
|
34
|
+
health_cmd = [openclaw_cmd, "health", "--json"]
|
|
35
|
+
|
|
36
|
+
if args.mode == "check-only":
|
|
37
|
+
return [inspect_cmd, update_probe_cmd]
|
|
38
|
+
|
|
39
|
+
if args.mode == "apply-update":
|
|
40
|
+
commands = [inspect_cmd, apply_cmd, doctor_cmd]
|
|
41
|
+
if args.allow_restart:
|
|
42
|
+
commands.append(restart_cmd)
|
|
43
|
+
commands.append(health_cmd)
|
|
44
|
+
return commands
|
|
45
|
+
|
|
46
|
+
if args.mode == "check-and-apply":
|
|
47
|
+
commands = [inspect_cmd, update_probe_cmd, apply_cmd, doctor_cmd]
|
|
48
|
+
if args.allow_restart:
|
|
49
|
+
commands.append(restart_cmd)
|
|
50
|
+
commands.append(health_cmd)
|
|
51
|
+
return commands
|
|
52
|
+
|
|
53
|
+
raise RuntimeError(f"unsupported mode: {args.mode}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
parser = argparse.ArgumentParser(description="Run the Grix/OpenClaw plugin update workflow.")
|
|
58
|
+
parser.add_argument("--mode", choices=["check-only", "apply-update", "check-and-apply"], default="check-and-apply")
|
|
59
|
+
parser.add_argument("--plugin-id", default="grix")
|
|
60
|
+
parser.add_argument("--allow-restart", default="true", choices=["true", "false"])
|
|
61
|
+
parser.add_argument("--openclaw", default="openclaw")
|
|
62
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
63
|
+
parser.add_argument("--json", action="store_true")
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
args.allow_restart = args.allow_restart == "true"
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
commands = build_plan(args)
|
|
69
|
+
results: list[dict[str, Any]] = []
|
|
70
|
+
if not args.dry_run:
|
|
71
|
+
for cmd in commands:
|
|
72
|
+
results.append(run_command(cmd))
|
|
73
|
+
payload = {
|
|
74
|
+
"ok": True,
|
|
75
|
+
"mode": args.mode,
|
|
76
|
+
"plugin_id": args.plugin_id,
|
|
77
|
+
"allow_restart": args.allow_restart,
|
|
78
|
+
"dry_run": bool(args.dry_run),
|
|
79
|
+
"commands": commands,
|
|
80
|
+
"results": results,
|
|
81
|
+
}
|
|
82
|
+
if args.json:
|
|
83
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
84
|
+
else:
|
|
85
|
+
print(f"mode={args.mode} plugin_id={args.plugin_id} dry_run={args.dry_run}")
|
|
86
|
+
for cmd in commands:
|
|
87
|
+
print("$ " + " ".join(cmd))
|
|
88
|
+
return 0
|
|
89
|
+
except Exception as exc: # noqa: BLE001
|
|
90
|
+
payload = {"ok": False, "error": str(exc)}
|
|
91
|
+
if args.json:
|
|
92
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
93
|
+
else:
|
|
94
|
+
print(str(exc), file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
raise SystemExit(main())
|