agentwatch-cli 0.1.0__py3-none-any.whl
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.
- agentwatch_cli/__init__.py +19 -0
- agentwatch_cli/__main__.py +6 -0
- agentwatch_cli/cli.py +522 -0
- agentwatch_cli/config.py +122 -0
- agentwatch_cli/connector.py +430 -0
- agentwatch_cli/gateway_client.py +307 -0
- agentwatch_cli/service.py +329 -0
- agentwatch_cli-0.1.0.dist-info/METADATA +461 -0
- agentwatch_cli-0.1.0.dist-info/RECORD +13 -0
- agentwatch_cli-0.1.0.dist-info/WHEEL +5 -0
- agentwatch_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentwatch_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentwatch_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentwatch-cli: Connect your local Moltbot gateway to AgentWatch cloud.
|
|
3
|
+
|
|
4
|
+
This package provides a connector that allows AgentWatch to communicate with
|
|
5
|
+
your local Moltbot (OpenClaw) gateway without exposing your local network.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "AgentWatch"
|
|
10
|
+
|
|
11
|
+
from .connector import MoltbotConnector
|
|
12
|
+
from .config import ConnectorConfig, load_config, save_config
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MoltbotConnector",
|
|
16
|
+
"ConnectorConfig",
|
|
17
|
+
"load_config",
|
|
18
|
+
"save_config",
|
|
19
|
+
]
|
agentwatch_cli/cli.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for agentwatch-cli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fix_script_permissions() -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Find and fix permissions on the agentwatch-cli script.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if permissions were fixed, False otherwise.
|
|
24
|
+
"""
|
|
25
|
+
# Find the script location
|
|
26
|
+
script_path = shutil.which("agentwatch-cli")
|
|
27
|
+
|
|
28
|
+
if not script_path:
|
|
29
|
+
# Try common locations
|
|
30
|
+
possible_paths = [
|
|
31
|
+
Path.home() / ".local" / "bin" / "agentwatch-cli",
|
|
32
|
+
Path.home() / "Library" / "Python" / "3.9" / "bin" / "agentwatch-cli",
|
|
33
|
+
Path.home() / "Library" / "Python" / "3.10" / "bin" / "agentwatch-cli",
|
|
34
|
+
Path.home() / "Library" / "Python" / "3.11" / "bin" / "agentwatch-cli",
|
|
35
|
+
Path.home() / "Library" / "Python" / "3.12" / "bin" / "agentwatch-cli",
|
|
36
|
+
]
|
|
37
|
+
for path in possible_paths:
|
|
38
|
+
if path.exists():
|
|
39
|
+
script_path = str(path)
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
if not script_path:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
path = Path(script_path)
|
|
47
|
+
# Add execute permission for owner, group, and others
|
|
48
|
+
current_mode = path.stat().st_mode
|
|
49
|
+
new_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
50
|
+
path.chmod(new_mode)
|
|
51
|
+
return True
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"Warning: Could not fix script permissions: {e}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
from .config import (
|
|
57
|
+
ConnectorConfig,
|
|
58
|
+
load_config,
|
|
59
|
+
save_config,
|
|
60
|
+
discover_gateway_token,
|
|
61
|
+
get_effective_gateway_token,
|
|
62
|
+
DEFAULT_CONFIG_FILE,
|
|
63
|
+
)
|
|
64
|
+
from .connector import MoltbotConnector, test_gateway_connection
|
|
65
|
+
from .service import install_service, uninstall_service, get_service_status
|
|
66
|
+
|
|
67
|
+
def find_openclaw_config() -> Optional[Path]:
|
|
68
|
+
"""Find the OpenClaw config file."""
|
|
69
|
+
# Check home directory first, then current directory
|
|
70
|
+
search_paths = [
|
|
71
|
+
Path.home() / ".openclaw" / "openclaw.json",
|
|
72
|
+
Path.cwd() / "openclaw.json",
|
|
73
|
+
]
|
|
74
|
+
for path in search_paths:
|
|
75
|
+
if path.exists():
|
|
76
|
+
return path
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def ensure_openclaw_http_enabled() -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Ensure OpenClaw's HTTP chat completions endpoint is enabled.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if config was updated, False if already enabled or file not found
|
|
86
|
+
"""
|
|
87
|
+
config_path = find_openclaw_config()
|
|
88
|
+
if not config_path:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
with open(config_path, "r") as f:
|
|
93
|
+
config = json.load(f)
|
|
94
|
+
|
|
95
|
+
# Navigate to gateway.http.endpoints.chatCompletions.enabled
|
|
96
|
+
# Create nested dicts if they don't exist
|
|
97
|
+
if "gateway" not in config:
|
|
98
|
+
config["gateway"] = {}
|
|
99
|
+
if "http" not in config["gateway"]:
|
|
100
|
+
config["gateway"]["http"] = {}
|
|
101
|
+
if "endpoints" not in config["gateway"]["http"]:
|
|
102
|
+
config["gateway"]["http"]["endpoints"] = {}
|
|
103
|
+
if "chatCompletions" not in config["gateway"]["http"]["endpoints"]:
|
|
104
|
+
config["gateway"]["http"]["endpoints"]["chatCompletions"] = {}
|
|
105
|
+
|
|
106
|
+
# Check if already enabled
|
|
107
|
+
if config["gateway"]["http"]["endpoints"]["chatCompletions"].get("enabled") == True:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# Enable it
|
|
111
|
+
config["gateway"]["http"]["endpoints"]["chatCompletions"]["enabled"] = True
|
|
112
|
+
|
|
113
|
+
with open(config_path, "w") as f:
|
|
114
|
+
json.dump(config, f, indent=2)
|
|
115
|
+
|
|
116
|
+
return True
|
|
117
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
118
|
+
print(f"Warning: Could not update OpenClaw config: {e}")
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normalize_enrollment_code(code: str) -> str:
|
|
123
|
+
"""Normalize enrollment code to XXXX-XXXX format."""
|
|
124
|
+
# Remove any dashes and whitespace, uppercase
|
|
125
|
+
clean = code.replace("-", "").replace(" ", "").upper()
|
|
126
|
+
if len(clean) == 8:
|
|
127
|
+
return f"{clean[:4]}-{clean[4:]}"
|
|
128
|
+
return code.upper()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def enroll_command(args: argparse.Namespace) -> int:
|
|
132
|
+
"""Handle the enroll command."""
|
|
133
|
+
enrollment_code = normalize_enrollment_code(args.code)
|
|
134
|
+
|
|
135
|
+
print(f"Enrolling with code: {enrollment_code}")
|
|
136
|
+
|
|
137
|
+
# Load existing config or create new
|
|
138
|
+
config = load_config()
|
|
139
|
+
|
|
140
|
+
# Determine enrollment endpoint
|
|
141
|
+
# First try environment variable, then default
|
|
142
|
+
import os
|
|
143
|
+
|
|
144
|
+
enrollment_url = os.environ.get(
|
|
145
|
+
"AGENTWATCH_ENROLLMENT_URL",
|
|
146
|
+
"https://agentwatch-api-production.up.railway.app/api/connector/enroll",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Call enrollment API
|
|
151
|
+
with httpx.Client(timeout=30.0) as client:
|
|
152
|
+
response = client.post(
|
|
153
|
+
enrollment_url,
|
|
154
|
+
json={"enrollment_code": enrollment_code},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if response.status_code == 429:
|
|
158
|
+
# Rate limited
|
|
159
|
+
try:
|
|
160
|
+
error_data = response.json()
|
|
161
|
+
retry_after = error_data.get('retry_after', 900)
|
|
162
|
+
print(f"Rate limited: Too many enrollment attempts.")
|
|
163
|
+
print(f"Please try again in {retry_after // 60} minutes.")
|
|
164
|
+
except json.JSONDecodeError:
|
|
165
|
+
print("Rate limited: Too many enrollment attempts. Please try again later.")
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
if response.status_code != 200:
|
|
169
|
+
try:
|
|
170
|
+
error_data = response.json()
|
|
171
|
+
print(f"Enrollment failed: {error_data.get('error', 'Unknown error')}")
|
|
172
|
+
except json.JSONDecodeError:
|
|
173
|
+
print(f"Enrollment failed: HTTP {response.status_code}")
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
data = response.json()
|
|
177
|
+
|
|
178
|
+
if not data.get("success"):
|
|
179
|
+
print(f"Enrollment failed: {data.get('error', 'Unknown error')}")
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
# Update config with enrollment data
|
|
183
|
+
config.connector_id = data["connector_id"]
|
|
184
|
+
config.secret = data["secret"]
|
|
185
|
+
config.agent_id = data["agent_id"]
|
|
186
|
+
config.agent_name = data["agent_name"]
|
|
187
|
+
config.agentwatch_url = data.get("agentwatch_url", config.agentwatch_url)
|
|
188
|
+
|
|
189
|
+
# Try to auto-discover gateway token
|
|
190
|
+
discovered_token = discover_gateway_token()
|
|
191
|
+
if discovered_token:
|
|
192
|
+
print("Auto-discovered gateway token from ~/.openclaw/openclaw.json")
|
|
193
|
+
# Don't save the token - let it be discovered each time for security
|
|
194
|
+
|
|
195
|
+
# Save config
|
|
196
|
+
save_config(config)
|
|
197
|
+
|
|
198
|
+
# Fix script permissions (needed for pipx on macOS)
|
|
199
|
+
if fix_script_permissions():
|
|
200
|
+
print("Fixed script permissions")
|
|
201
|
+
|
|
202
|
+
# Enable OpenClaw HTTP endpoint
|
|
203
|
+
if ensure_openclaw_http_enabled():
|
|
204
|
+
print("Enabled OpenClaw HTTP chat completions endpoint")
|
|
205
|
+
print("Note: You may need to restart OpenClaw for changes to take effect")
|
|
206
|
+
|
|
207
|
+
print()
|
|
208
|
+
print("=" * 50)
|
|
209
|
+
print("Enrollment successful!")
|
|
210
|
+
print("=" * 50)
|
|
211
|
+
print(f"Agent: {config.agent_name}")
|
|
212
|
+
print(f"Config saved to: {DEFAULT_CONFIG_FILE}")
|
|
213
|
+
print()
|
|
214
|
+
print("To start the connector, run:")
|
|
215
|
+
print()
|
|
216
|
+
print(" agentwatch-cli start")
|
|
217
|
+
print()
|
|
218
|
+
print("Or install as a background service:")
|
|
219
|
+
print()
|
|
220
|
+
print(" agentwatch-cli install-service")
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
except httpx.ConnectError:
|
|
226
|
+
print(f"Failed to connect to enrollment server at {enrollment_url}")
|
|
227
|
+
print("Please check your internet connection.")
|
|
228
|
+
return 1
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(f"Enrollment error: {e}")
|
|
231
|
+
return 1
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def start_command(args: argparse.Namespace) -> int:
|
|
235
|
+
"""Handle the start command."""
|
|
236
|
+
config = load_config()
|
|
237
|
+
|
|
238
|
+
if not config.is_enrolled():
|
|
239
|
+
print("Connector is not enrolled.")
|
|
240
|
+
print("Please run: agentwatch-cli enroll --code <YOUR_CODE>")
|
|
241
|
+
return 1
|
|
242
|
+
|
|
243
|
+
# Apply command line overrides
|
|
244
|
+
if args.gateway_url:
|
|
245
|
+
config.gateway_url = args.gateway_url
|
|
246
|
+
if args.gateway_token:
|
|
247
|
+
config.gateway_token = args.gateway_token
|
|
248
|
+
|
|
249
|
+
print(f"Starting connector for agent: {config.agent_name}")
|
|
250
|
+
print(f"Local gateway: {config.gateway_url}")
|
|
251
|
+
print(f"AgentWatch cloud: {config.agentwatch_url}")
|
|
252
|
+
print()
|
|
253
|
+
|
|
254
|
+
# Test gateway connection first
|
|
255
|
+
async def test_and_run():
|
|
256
|
+
# Test gateway
|
|
257
|
+
if not await test_gateway_connection(config):
|
|
258
|
+
print(f"Cannot connect to local gateway at {config.gateway_url}")
|
|
259
|
+
print("Please make sure your Moltbot gateway is running.")
|
|
260
|
+
return 1
|
|
261
|
+
|
|
262
|
+
print("Local gateway connection: OK")
|
|
263
|
+
print()
|
|
264
|
+
|
|
265
|
+
# Start connector
|
|
266
|
+
connector = MoltbotConnector(config)
|
|
267
|
+
await connector.run()
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
return asyncio.run(test_and_run())
|
|
272
|
+
except KeyboardInterrupt:
|
|
273
|
+
print("\nStopped by user")
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def status_command(args: argparse.Namespace) -> int:
|
|
278
|
+
"""Handle the status command."""
|
|
279
|
+
config = load_config()
|
|
280
|
+
|
|
281
|
+
print("AgentWatch CLI Connector Status")
|
|
282
|
+
print("=" * 40)
|
|
283
|
+
|
|
284
|
+
if config.is_enrolled():
|
|
285
|
+
print(f"Enrolled: Yes")
|
|
286
|
+
print(f"Agent: {config.agent_name}")
|
|
287
|
+
print(f"Agent ID: {config.agent_id}")
|
|
288
|
+
print(f"Connector ID: {config.connector_id[:8]}...")
|
|
289
|
+
else:
|
|
290
|
+
print("Enrolled: No")
|
|
291
|
+
print()
|
|
292
|
+
print("Run 'agentwatch-cli enroll --code <CODE>' to enroll")
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
print()
|
|
296
|
+
print(f"Gateway URL: {config.gateway_url}")
|
|
297
|
+
print(f"AgentWatch URL: {config.agentwatch_url}")
|
|
298
|
+
|
|
299
|
+
# Check gateway token
|
|
300
|
+
token = get_effective_gateway_token(config)
|
|
301
|
+
if token:
|
|
302
|
+
print(f"Gateway Token: {'<configured>' if config.gateway_token else '<auto-discovered>'}")
|
|
303
|
+
else:
|
|
304
|
+
print("Gateway Token: NOT FOUND")
|
|
305
|
+
print(" Run: agentwatch-cli config --gateway-token <TOKEN>")
|
|
306
|
+
print(" Or ensure ~/.openclaw/openclaw.json exists")
|
|
307
|
+
|
|
308
|
+
print()
|
|
309
|
+
|
|
310
|
+
# Test gateway connection
|
|
311
|
+
print("Testing gateway connection...")
|
|
312
|
+
|
|
313
|
+
async def test():
|
|
314
|
+
return await test_gateway_connection(config)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
is_healthy = asyncio.run(test())
|
|
318
|
+
if is_healthy:
|
|
319
|
+
print("Gateway Status: ONLINE")
|
|
320
|
+
else:
|
|
321
|
+
print("Gateway Status: OFFLINE or UNREACHABLE")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
print(f"Gateway Status: ERROR ({e})")
|
|
324
|
+
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def config_command(args: argparse.Namespace) -> int:
|
|
329
|
+
"""Handle the config command."""
|
|
330
|
+
config = load_config()
|
|
331
|
+
|
|
332
|
+
if args.gateway_url:
|
|
333
|
+
config.gateway_url = args.gateway_url
|
|
334
|
+
print(f"Set gateway_url = {args.gateway_url}")
|
|
335
|
+
|
|
336
|
+
if args.gateway_token:
|
|
337
|
+
config.gateway_token = args.gateway_token
|
|
338
|
+
print(f"Set gateway_token = <hidden>")
|
|
339
|
+
|
|
340
|
+
if args.agentwatch_url:
|
|
341
|
+
config.agentwatch_url = args.agentwatch_url
|
|
342
|
+
print(f"Set agentwatch_url = {args.agentwatch_url}")
|
|
343
|
+
|
|
344
|
+
# Save updated config
|
|
345
|
+
save_config(config)
|
|
346
|
+
print(f"Configuration saved to {DEFAULT_CONFIG_FILE}")
|
|
347
|
+
|
|
348
|
+
return 0
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def revoke_command(args: argparse.Namespace) -> int:
|
|
352
|
+
"""Handle the revoke command (clear enrollment)."""
|
|
353
|
+
config = load_config()
|
|
354
|
+
|
|
355
|
+
if not config.is_enrolled():
|
|
356
|
+
print("Connector is not enrolled.")
|
|
357
|
+
return 0
|
|
358
|
+
|
|
359
|
+
# Confirm
|
|
360
|
+
if not args.force:
|
|
361
|
+
response = input(
|
|
362
|
+
f"This will revoke enrollment for agent '{config.agent_name}'. Continue? [y/N] "
|
|
363
|
+
)
|
|
364
|
+
if response.lower() != "y":
|
|
365
|
+
print("Cancelled.")
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
# Clear enrollment data
|
|
369
|
+
config.connector_id = None
|
|
370
|
+
config.secret = None
|
|
371
|
+
config.agent_id = None
|
|
372
|
+
config.agent_name = None
|
|
373
|
+
|
|
374
|
+
save_config(config)
|
|
375
|
+
|
|
376
|
+
print("Enrollment revoked. You will need to re-enroll to use the connector.")
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def install_service_command(args: argparse.Namespace) -> int:
|
|
381
|
+
"""Handle the install-service command."""
|
|
382
|
+
config = load_config()
|
|
383
|
+
|
|
384
|
+
if not config.is_enrolled():
|
|
385
|
+
print("Error: Connector is not enrolled.")
|
|
386
|
+
print("Please run 'agentwatch-cli enroll --code <CODE>' first.")
|
|
387
|
+
return 1
|
|
388
|
+
|
|
389
|
+
print("Installing agentwatch-cli as a system service...")
|
|
390
|
+
print()
|
|
391
|
+
|
|
392
|
+
success, message = install_service(user=getattr(args, 'user', None))
|
|
393
|
+
|
|
394
|
+
print(message)
|
|
395
|
+
return 0 if success else 1
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def uninstall_service_command(args: argparse.Namespace) -> int:
|
|
399
|
+
"""Handle the uninstall-service command."""
|
|
400
|
+
print("Uninstalling agentwatch-cli service...")
|
|
401
|
+
|
|
402
|
+
success, message = uninstall_service()
|
|
403
|
+
|
|
404
|
+
print(message)
|
|
405
|
+
return 0 if success else 1
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def service_status_command(args: argparse.Namespace) -> int:
|
|
409
|
+
"""Handle the service-status command."""
|
|
410
|
+
is_running, message = get_service_status()
|
|
411
|
+
|
|
412
|
+
print("AgentWatch CLI Service Status")
|
|
413
|
+
print("=" * 40)
|
|
414
|
+
print(message)
|
|
415
|
+
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main() -> int:
|
|
420
|
+
"""Main CLI entry point."""
|
|
421
|
+
parser = argparse.ArgumentParser(
|
|
422
|
+
prog="agentwatch-cli",
|
|
423
|
+
description="Connect your local Moltbot gateway to AgentWatch cloud",
|
|
424
|
+
)
|
|
425
|
+
parser.add_argument(
|
|
426
|
+
"--version", action="version", version="%(prog)s 0.1.0"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
430
|
+
|
|
431
|
+
# enroll command
|
|
432
|
+
enroll_parser = subparsers.add_parser(
|
|
433
|
+
"enroll", help="Enroll connector with an enrollment code"
|
|
434
|
+
)
|
|
435
|
+
enroll_parser.add_argument(
|
|
436
|
+
"--code", "-c", required=True, help="Enrollment code from AgentWatch"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# start command
|
|
440
|
+
start_parser = subparsers.add_parser(
|
|
441
|
+
"start", help="Start the connector"
|
|
442
|
+
)
|
|
443
|
+
start_parser.add_argument(
|
|
444
|
+
"--gateway-url", help="Override gateway URL"
|
|
445
|
+
)
|
|
446
|
+
start_parser.add_argument(
|
|
447
|
+
"--gateway-token", help="Override gateway token"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# status command
|
|
451
|
+
subparsers.add_parser(
|
|
452
|
+
"status", help="Show connector status"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# config command
|
|
456
|
+
config_parser = subparsers.add_parser(
|
|
457
|
+
"config", help="Configure connector settings"
|
|
458
|
+
)
|
|
459
|
+
config_parser.add_argument(
|
|
460
|
+
"--gateway-url", help="Set gateway URL"
|
|
461
|
+
)
|
|
462
|
+
config_parser.add_argument(
|
|
463
|
+
"--gateway-token", help="Set gateway token"
|
|
464
|
+
)
|
|
465
|
+
config_parser.add_argument(
|
|
466
|
+
"--agentwatch-url", help="Set AgentWatch cloud URL"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# revoke command
|
|
470
|
+
revoke_parser = subparsers.add_parser(
|
|
471
|
+
"revoke", help="Revoke enrollment"
|
|
472
|
+
)
|
|
473
|
+
revoke_parser.add_argument(
|
|
474
|
+
"--force", "-f", action="store_true", help="Skip confirmation"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# install-service command
|
|
478
|
+
install_service_parser = subparsers.add_parser(
|
|
479
|
+
"install-service", help="Install as a system service (auto-start on boot)"
|
|
480
|
+
)
|
|
481
|
+
install_service_parser.add_argument(
|
|
482
|
+
"--user", help="User to run the service as (Linux only, default: current user)"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# uninstall-service command
|
|
486
|
+
subparsers.add_parser(
|
|
487
|
+
"uninstall-service", help="Uninstall the system service"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# service-status command
|
|
491
|
+
subparsers.add_parser(
|
|
492
|
+
"service-status", help="Check the system service status"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
args = parser.parse_args()
|
|
496
|
+
|
|
497
|
+
if args.command is None:
|
|
498
|
+
parser.print_help()
|
|
499
|
+
return 0
|
|
500
|
+
|
|
501
|
+
# Dispatch to command handler
|
|
502
|
+
handlers = {
|
|
503
|
+
"enroll": enroll_command,
|
|
504
|
+
"start": start_command,
|
|
505
|
+
"status": status_command,
|
|
506
|
+
"config": config_command,
|
|
507
|
+
"revoke": revoke_command,
|
|
508
|
+
"install-service": install_service_command,
|
|
509
|
+
"uninstall-service": uninstall_service_command,
|
|
510
|
+
"service-status": service_status_command,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
handler = handlers.get(args.command)
|
|
514
|
+
if handler:
|
|
515
|
+
return handler(args)
|
|
516
|
+
else:
|
|
517
|
+
parser.print_help()
|
|
518
|
+
return 1
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
if __name__ == "__main__":
|
|
522
|
+
sys.exit(main())
|
agentwatch_cli/config.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for agentwatch-cli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass, asdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Default paths
|
|
13
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".agentwatch-cli"
|
|
14
|
+
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.json"
|
|
15
|
+
OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ConnectorConfig:
|
|
20
|
+
"""Configuration for the agentwatch-cli connector."""
|
|
21
|
+
|
|
22
|
+
# Credentials (set after enrollment)
|
|
23
|
+
connector_id: Optional[str] = None
|
|
24
|
+
secret: Optional[str] = None
|
|
25
|
+
agent_id: Optional[str] = None
|
|
26
|
+
agent_name: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
# AgentWatch cloud URL
|
|
29
|
+
agentwatch_url: str = "wss://agentwatch.helivan.io"
|
|
30
|
+
|
|
31
|
+
# Local OpenClaw gateway configuration (WebSocket)
|
|
32
|
+
gateway_url: str = "ws://127.0.0.1:18789"
|
|
33
|
+
gateway_token: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
def is_enrolled(self) -> bool:
|
|
36
|
+
"""Check if the connector is enrolled."""
|
|
37
|
+
return bool(self.connector_id and self.secret and self.agent_id)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_config(config_path: Optional[Path] = None) -> ConnectorConfig:
|
|
41
|
+
"""Load configuration from file."""
|
|
42
|
+
path = config_path or DEFAULT_CONFIG_FILE
|
|
43
|
+
|
|
44
|
+
if not path.exists():
|
|
45
|
+
return ConnectorConfig()
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(path, "r") as f:
|
|
49
|
+
data = json.load(f)
|
|
50
|
+
return ConnectorConfig(**data)
|
|
51
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
52
|
+
print(f"Warning: Failed to load config from {path}: {e}")
|
|
53
|
+
return ConnectorConfig()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_config(config: ConnectorConfig, config_path: Optional[Path] = None) -> None:
|
|
57
|
+
"""Save configuration to file."""
|
|
58
|
+
path = config_path or DEFAULT_CONFIG_FILE
|
|
59
|
+
|
|
60
|
+
# Create directory if it doesn't exist
|
|
61
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
with open(path, "w") as f:
|
|
64
|
+
json.dump(asdict(config), f, indent=2)
|
|
65
|
+
|
|
66
|
+
# Set restrictive permissions (only owner can read/write)
|
|
67
|
+
os.chmod(path, 0o600)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def discover_gateway_token() -> Optional[str]:
|
|
71
|
+
"""
|
|
72
|
+
Auto-discover gateway token from openclaw.json.
|
|
73
|
+
|
|
74
|
+
Checks in order:
|
|
75
|
+
1. Current directory: ./openclaw.json
|
|
76
|
+
2. Home directory: ~/.openclaw/openclaw.json
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The gateway token if found, None otherwise.
|
|
80
|
+
"""
|
|
81
|
+
# Check home directory first, then current directory
|
|
82
|
+
search_paths = [
|
|
83
|
+
OPENCLAW_CONFIG_PATH,
|
|
84
|
+
Path.cwd() / "openclaw.json",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
for config_path in search_paths:
|
|
88
|
+
if not config_path.exists():
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
with open(config_path, "r") as f:
|
|
93
|
+
openclaw_config = json.load(f)
|
|
94
|
+
|
|
95
|
+
# Navigate to gateway.auth.token
|
|
96
|
+
token = (
|
|
97
|
+
openclaw_config.get("gateway", {})
|
|
98
|
+
.get("auth", {})
|
|
99
|
+
.get("token")
|
|
100
|
+
)
|
|
101
|
+
if token:
|
|
102
|
+
return token
|
|
103
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_effective_gateway_token(config: ConnectorConfig) -> Optional[str]:
|
|
110
|
+
"""
|
|
111
|
+
Get the effective gateway token, preferring config over auto-discovery.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: The connector configuration.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The gateway token from config, or auto-discovered from OpenClaw config.
|
|
118
|
+
"""
|
|
119
|
+
if config.gateway_token:
|
|
120
|
+
return config.gateway_token
|
|
121
|
+
|
|
122
|
+
return discover_gateway_token()
|