@gaia-minds/assistant-cli 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.
- package/LICENSE +41 -0
- package/README.md +200 -0
- package/assistant/README.md +117 -0
- package/bin/gaia.js +67 -0
- package/package.json +43 -0
- package/tools/agent-config.yml +129 -0
- package/tools/agent-loop.py +912 -0
- package/tools/gaia-assistant.py +895 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Standalone Gaia personal assistant launcher.
|
|
3
|
+
|
|
4
|
+
This wrapper makes it easier for users to run Gaia's dual-track evolution loop
|
|
5
|
+
as a personal assistant runtime without OpenClaw as a hard dependency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
23
|
+
REPO_ROOT = SCRIPT_DIR.parent
|
|
24
|
+
DEFAULT_HOME = Path(os.environ.get("GAIA_ASSISTANT_HOME", str(Path.home() / ".gaia-assistant"))).expanduser()
|
|
25
|
+
DEFAULT_STATE_DIR = DEFAULT_HOME / "state"
|
|
26
|
+
DEFAULT_CONFIG_PATH = DEFAULT_HOME / "config.json"
|
|
27
|
+
AGENT_CONFIG_PATH = SCRIPT_DIR / "agent-config.yml"
|
|
28
|
+
AGENT_LOOP_PATH = SCRIPT_DIR / "agent-loop.py"
|
|
29
|
+
DEFAULT_LAUNCHER_HINT = "python3 tools/gaia-assistant.py"
|
|
30
|
+
DEFAULT_GAIA_AUTH_STORE = DEFAULT_HOME / "auth-profiles.json"
|
|
31
|
+
DEFAULT_CODEX_AUTH_PATH = Path(
|
|
32
|
+
os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))
|
|
33
|
+
).expanduser() / "auth.json"
|
|
34
|
+
DEFAULT_OPENCLAW_STATE_DIR = Path(
|
|
35
|
+
os.environ.get("OPENCLAW_STATE_DIR", str(Path.home() / ".openclaw"))
|
|
36
|
+
).expanduser()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
DEFAULT_CONFIG: Dict[str, Any] = {
|
|
40
|
+
"runtime": {
|
|
41
|
+
"mode": "continuous",
|
|
42
|
+
"interval_minutes": 60,
|
|
43
|
+
},
|
|
44
|
+
"auth": {
|
|
45
|
+
"providers": {
|
|
46
|
+
"anthropic": {
|
|
47
|
+
"subscription_oauth_supported": True,
|
|
48
|
+
"api_key_env": "ANTHROPIC_API_KEY",
|
|
49
|
+
},
|
|
50
|
+
"openai": {
|
|
51
|
+
"subscription_oauth_supported": True,
|
|
52
|
+
"api_key_env": "OPENAI_API_KEY",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"tracks": {
|
|
57
|
+
"default": "auto",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _launcher_hint() -> str:
|
|
63
|
+
hint = os.environ.get("GAIA_ASSISTANT_CLI_HINT", "").strip()
|
|
64
|
+
return hint if hint else DEFAULT_LAUNCHER_HINT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_json(path: Path) -> Dict[str, Any]:
|
|
68
|
+
if not path.exists():
|
|
69
|
+
return {}
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
77
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _write_secret_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
82
|
+
_write_json(path, payload)
|
|
83
|
+
try:
|
|
84
|
+
os.chmod(path, 0o600)
|
|
85
|
+
except OSError:
|
|
86
|
+
# Best effort: on some filesystems chmod may not be supported.
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _normalize_config(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
91
|
+
cfg: Dict[str, Any] = payload if isinstance(payload, dict) else {}
|
|
92
|
+
|
|
93
|
+
runtime = cfg.setdefault("runtime", {})
|
|
94
|
+
runtime.setdefault("mode", "continuous")
|
|
95
|
+
runtime.setdefault("interval_minutes", 60)
|
|
96
|
+
|
|
97
|
+
auth = cfg.setdefault("auth", {})
|
|
98
|
+
providers = auth.setdefault("providers", {})
|
|
99
|
+
auth.setdefault("store_path", str(DEFAULT_GAIA_AUTH_STORE))
|
|
100
|
+
default_providers = DEFAULT_CONFIG.get("auth", {}).get("providers", {})
|
|
101
|
+
for provider_name, provider_defaults in default_providers.items():
|
|
102
|
+
provider_cfg = providers.setdefault(provider_name, {})
|
|
103
|
+
if isinstance(provider_cfg, dict):
|
|
104
|
+
for key, value in provider_defaults.items():
|
|
105
|
+
provider_cfg.setdefault(key, value)
|
|
106
|
+
auth.setdefault("active_profile", None)
|
|
107
|
+
|
|
108
|
+
tracks = cfg.setdefault("tracks", {})
|
|
109
|
+
tracks.setdefault("default", "auto")
|
|
110
|
+
return cfg
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ensure_config_exists(cfg_path: Path) -> Dict[str, Any]:
|
|
114
|
+
cfg = _load_json(cfg_path)
|
|
115
|
+
if not cfg:
|
|
116
|
+
cfg = _normalize_config(dict(DEFAULT_CONFIG))
|
|
117
|
+
_write_json(cfg_path, cfg)
|
|
118
|
+
return cfg
|
|
119
|
+
|
|
120
|
+
cfg = _normalize_config(cfg)
|
|
121
|
+
_write_json(cfg_path, cfg)
|
|
122
|
+
return cfg
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _resolve_openclaw_auth_store(openclaw_agent: str, override_path: Optional[str]) -> Path:
|
|
126
|
+
if override_path:
|
|
127
|
+
return Path(override_path).expanduser()
|
|
128
|
+
return DEFAULT_OPENCLAW_STATE_DIR / "agents" / openclaw_agent / "agent" / "auth-profiles.json"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _resolve_codex_auth_path(override_path: Optional[str]) -> Path:
|
|
132
|
+
if override_path:
|
|
133
|
+
return Path(override_path).expanduser()
|
|
134
|
+
return DEFAULT_CODEX_AUTH_PATH
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve_gaia_auth_store(cfg_path: Path, override_path: Optional[str]) -> Path:
|
|
138
|
+
if override_path:
|
|
139
|
+
return Path(override_path).expanduser()
|
|
140
|
+
cfg = _load_json(cfg_path)
|
|
141
|
+
if cfg:
|
|
142
|
+
store = cfg.get("auth", {}).get("store_path")
|
|
143
|
+
if isinstance(store, str) and store.strip():
|
|
144
|
+
return Path(store).expanduser()
|
|
145
|
+
return cfg_path.parent / "auth-profiles.json"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _load_gaia_auth_store(path: Path) -> Dict[str, Any]:
|
|
149
|
+
payload = _load_json(path)
|
|
150
|
+
if not payload:
|
|
151
|
+
return {"version": 1, "profiles": {}}
|
|
152
|
+
if not isinstance(payload.get("profiles"), dict):
|
|
153
|
+
payload["profiles"] = {}
|
|
154
|
+
if "version" not in payload:
|
|
155
|
+
payload["version"] = 1
|
|
156
|
+
return payload
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _save_gaia_auth_store(path: Path, payload: Dict[str, Any]) -> None:
|
|
160
|
+
_write_secret_json(path, payload)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _decode_jwt_payload(token: str) -> Dict[str, Any]:
|
|
164
|
+
parts = token.split(".")
|
|
165
|
+
if len(parts) < 2:
|
|
166
|
+
return {}
|
|
167
|
+
payload = parts[1]
|
|
168
|
+
payload += "=" * (-len(payload) % 4)
|
|
169
|
+
try:
|
|
170
|
+
raw = base64.urlsafe_b64decode(payload.encode("ascii")).decode("utf-8")
|
|
171
|
+
claims = json.loads(raw)
|
|
172
|
+
return claims if isinstance(claims, dict) else {}
|
|
173
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _read_codex_cli_credentials(codex_auth_path: Path) -> Optional[Dict[str, Any]]:
|
|
178
|
+
payload = _load_json(codex_auth_path)
|
|
179
|
+
if not payload:
|
|
180
|
+
return None
|
|
181
|
+
tokens = payload.get("tokens")
|
|
182
|
+
if not isinstance(tokens, dict):
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
access = tokens.get("access_token")
|
|
186
|
+
refresh = tokens.get("refresh_token")
|
|
187
|
+
account_id = tokens.get("account_id")
|
|
188
|
+
if not isinstance(access, str) or not access.strip():
|
|
189
|
+
return None
|
|
190
|
+
if not isinstance(refresh, str) or not refresh.strip():
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
claims = _decode_jwt_payload(access)
|
|
194
|
+
exp_s = claims.get("exp")
|
|
195
|
+
expires_ms: int
|
|
196
|
+
if isinstance(exp_s, (int, float)):
|
|
197
|
+
expires_ms = int(exp_s * 1000)
|
|
198
|
+
else:
|
|
199
|
+
# Conservative fallback if JWT claims aren't available.
|
|
200
|
+
expires_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + 3600 * 1000
|
|
201
|
+
|
|
202
|
+
profile_claims = claims.get("https://api.openai.com/profile")
|
|
203
|
+
email = ""
|
|
204
|
+
if isinstance(profile_claims, dict):
|
|
205
|
+
raw_email = profile_claims.get("email")
|
|
206
|
+
if isinstance(raw_email, str):
|
|
207
|
+
email = raw_email.strip()
|
|
208
|
+
|
|
209
|
+
auth_claims = claims.get("https://api.openai.com/auth")
|
|
210
|
+
if not account_id and isinstance(auth_claims, dict):
|
|
211
|
+
auth_account_id = auth_claims.get("chatgpt_account_id")
|
|
212
|
+
if isinstance(auth_account_id, str) and auth_account_id.strip():
|
|
213
|
+
account_id = auth_account_id.strip()
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"type": "oauth",
|
|
217
|
+
"provider": "openai-codex",
|
|
218
|
+
"access": access,
|
|
219
|
+
"refresh": refresh,
|
|
220
|
+
"expires": expires_ms,
|
|
221
|
+
"account_id": str(account_id).strip() if isinstance(account_id, str) else "",
|
|
222
|
+
"email": email,
|
|
223
|
+
"source": "codex-cli",
|
|
224
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _load_openclaw_profiles(path: Path) -> Dict[str, Any]:
|
|
229
|
+
payload = _load_json(path)
|
|
230
|
+
profiles = payload.get("profiles", {})
|
|
231
|
+
return profiles if isinstance(profiles, dict) else {}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _is_expired(credential: Dict[str, Any]) -> bool:
|
|
235
|
+
expires = credential.get("expires")
|
|
236
|
+
if not isinstance(expires, (int, float)):
|
|
237
|
+
return False
|
|
238
|
+
return int(expires) <= int(datetime.now(timezone.utc).timestamp() * 1000)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _format_expiry(credential: Dict[str, Any]) -> str:
|
|
242
|
+
expires = credential.get("expires")
|
|
243
|
+
if not isinstance(expires, (int, float)):
|
|
244
|
+
return "unknown"
|
|
245
|
+
try:
|
|
246
|
+
dt = datetime.fromtimestamp(int(expires) / 1000, tz=timezone.utc)
|
|
247
|
+
return dt.isoformat()
|
|
248
|
+
except (OSError, ValueError):
|
|
249
|
+
return "unknown"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _collect_provider_profiles(profiles: Dict[str, Any], provider: str) -> Dict[str, Dict[str, Any]]:
|
|
253
|
+
out: Dict[str, Dict[str, Any]] = {}
|
|
254
|
+
for profile_id, credential in profiles.items():
|
|
255
|
+
if not isinstance(credential, dict):
|
|
256
|
+
continue
|
|
257
|
+
if credential.get("provider") != provider:
|
|
258
|
+
continue
|
|
259
|
+
out[profile_id] = credential
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _pick_profile_id(profiles: Dict[str, Dict[str, Any]]) -> Optional[str]:
|
|
264
|
+
if not profiles:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
ranked: List[Tuple[int, int, str]] = []
|
|
268
|
+
for profile_id, credential in profiles.items():
|
|
269
|
+
expires = credential.get("expires")
|
|
270
|
+
exp = int(expires) if isinstance(expires, (int, float)) else 0
|
|
271
|
+
expired = 1 if _is_expired(credential) else 0
|
|
272
|
+
ranked.append((expired, -exp, profile_id))
|
|
273
|
+
ranked.sort()
|
|
274
|
+
return ranked[0][2]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _profile_id_for_credential(provider: str, credential: Dict[str, Any]) -> str:
|
|
278
|
+
email = credential.get("email")
|
|
279
|
+
if isinstance(email, str) and email.strip():
|
|
280
|
+
return f"{provider}:{email.strip().lower()}"
|
|
281
|
+
account_id = credential.get("account_id")
|
|
282
|
+
if isinstance(account_id, str) and account_id.strip():
|
|
283
|
+
return f"{provider}:{account_id.strip()}"
|
|
284
|
+
return f"{provider}:default"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _link_profile(
|
|
288
|
+
cfg_path: Path,
|
|
289
|
+
provider: str,
|
|
290
|
+
profile_id: str,
|
|
291
|
+
source: str,
|
|
292
|
+
store_path: Path,
|
|
293
|
+
) -> int:
|
|
294
|
+
cfg = _ensure_config_exists(cfg_path)
|
|
295
|
+
auth = cfg.setdefault("auth", {})
|
|
296
|
+
auth["store_path"] = str(store_path)
|
|
297
|
+
auth["active_profile"] = {
|
|
298
|
+
"provider": provider,
|
|
299
|
+
"profile_id": profile_id,
|
|
300
|
+
"source": source,
|
|
301
|
+
"store_path": str(store_path),
|
|
302
|
+
"linked_at": datetime.now(timezone.utc).isoformat(),
|
|
303
|
+
}
|
|
304
|
+
_write_json(cfg_path, cfg)
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _codex_login() -> int:
|
|
309
|
+
if not shutil.which("codex"):
|
|
310
|
+
print(
|
|
311
|
+
"Codex CLI is not installed. Install it, then run:\n"
|
|
312
|
+
" codex login",
|
|
313
|
+
file=sys.stderr,
|
|
314
|
+
)
|
|
315
|
+
return 1
|
|
316
|
+
cmd = ["codex", "login", "--device-auth"]
|
|
317
|
+
return subprocess.run(cmd).returncode
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _import_codex_profile_to_gaia(
|
|
321
|
+
cfg_path: Path,
|
|
322
|
+
provider: str,
|
|
323
|
+
codex_auth_path: Path,
|
|
324
|
+
gaia_auth_store: Path,
|
|
325
|
+
) -> Tuple[int, str]:
|
|
326
|
+
if provider != "openai-codex":
|
|
327
|
+
return 1, ""
|
|
328
|
+
credential = _read_codex_cli_credentials(codex_auth_path)
|
|
329
|
+
if credential is None:
|
|
330
|
+
print(
|
|
331
|
+
"Codex credentials not found after login.\n"
|
|
332
|
+
f"Expected auth file: {codex_auth_path}",
|
|
333
|
+
file=sys.stderr,
|
|
334
|
+
)
|
|
335
|
+
return 1, ""
|
|
336
|
+
|
|
337
|
+
store = _load_gaia_auth_store(gaia_auth_store)
|
|
338
|
+
profiles = store.setdefault("profiles", {})
|
|
339
|
+
if not isinstance(profiles, dict):
|
|
340
|
+
profiles = {}
|
|
341
|
+
store["profiles"] = profiles
|
|
342
|
+
|
|
343
|
+
profile_id = _profile_id_for_credential(provider, credential)
|
|
344
|
+
profiles[profile_id] = credential
|
|
345
|
+
_save_gaia_auth_store(gaia_auth_store, store)
|
|
346
|
+
_link_profile(cfg_path, provider, profile_id, "gaia-local", gaia_auth_store)
|
|
347
|
+
return 0, profile_id
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _link_openclaw_profile(
|
|
351
|
+
cfg_path: Path,
|
|
352
|
+
provider: str,
|
|
353
|
+
profile_id: str,
|
|
354
|
+
openclaw_store: Path,
|
|
355
|
+
) -> int:
|
|
356
|
+
return _link_profile(cfg_path, provider, profile_id, "openclaw", openclaw_store)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _read_linked_credential(active_profile: Dict[str, Any]) -> Tuple[Optional[Dict[str, Any]], str]:
|
|
360
|
+
source = str(active_profile.get("source", "")).strip()
|
|
361
|
+
provider = str(active_profile.get("provider", "")).strip()
|
|
362
|
+
profile_id = str(active_profile.get("profile_id", "")).strip()
|
|
363
|
+
store_raw = str(active_profile.get("store_path", "")).strip()
|
|
364
|
+
if not store_raw:
|
|
365
|
+
return None, "linked profile store_path is empty"
|
|
366
|
+
|
|
367
|
+
store_path = Path(store_raw).expanduser()
|
|
368
|
+
if not store_path.exists():
|
|
369
|
+
return None, f"linked profile store not found: {store_path}"
|
|
370
|
+
|
|
371
|
+
if source == "gaia-local":
|
|
372
|
+
profiles = _load_gaia_auth_store(store_path).get("profiles", {})
|
|
373
|
+
elif source == "openclaw":
|
|
374
|
+
profiles = _load_openclaw_profiles(store_path)
|
|
375
|
+
else:
|
|
376
|
+
return None, f"unknown auth source: {source}"
|
|
377
|
+
|
|
378
|
+
if not isinstance(profiles, dict):
|
|
379
|
+
return None, f"profile store is invalid: {store_path}"
|
|
380
|
+
|
|
381
|
+
credential = profiles.get(profile_id)
|
|
382
|
+
if not isinstance(credential, dict):
|
|
383
|
+
return None, f"linked profile missing in store: {profile_id} ({store_path})"
|
|
384
|
+
|
|
385
|
+
if provider and credential.get("provider") != provider:
|
|
386
|
+
return None, (
|
|
387
|
+
f"linked profile provider mismatch for {profile_id}: "
|
|
388
|
+
f"expected {provider}, got {credential.get('provider')}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return credential, ""
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
395
|
+
cfg_path = Path(args.config).expanduser()
|
|
396
|
+
state_dir = Path(args.state_dir).expanduser()
|
|
397
|
+
try:
|
|
398
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
399
|
+
except PermissionError:
|
|
400
|
+
print(
|
|
401
|
+
"Cannot create state directory due to permissions. "
|
|
402
|
+
"Set GAIA_ASSISTANT_HOME or pass --state-dir to a writable path.",
|
|
403
|
+
file=sys.stderr,
|
|
404
|
+
)
|
|
405
|
+
return 1
|
|
406
|
+
if cfg_path.exists() and not args.force:
|
|
407
|
+
print(
|
|
408
|
+
f"Config already exists at {cfg_path}. Use --force to overwrite.",
|
|
409
|
+
file=sys.stderr,
|
|
410
|
+
)
|
|
411
|
+
return 1
|
|
412
|
+
try:
|
|
413
|
+
_write_json(cfg_path, DEFAULT_CONFIG)
|
|
414
|
+
except PermissionError:
|
|
415
|
+
print(
|
|
416
|
+
"Cannot write config due to permissions. "
|
|
417
|
+
"Set GAIA_ASSISTANT_HOME or pass --config to a writable path.",
|
|
418
|
+
file=sys.stderr,
|
|
419
|
+
)
|
|
420
|
+
return 1
|
|
421
|
+
print(f"Initialized Gaia assistant config: {cfg_path}")
|
|
422
|
+
print(f"Initialized Gaia assistant state dir: {state_dir}")
|
|
423
|
+
return 0
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
427
|
+
problems = 0
|
|
428
|
+
required_cmds = ["python3", "git"]
|
|
429
|
+
optional_cmds = ["gh", "codex", "openclaw"]
|
|
430
|
+
cfg_path = Path(args.config).expanduser()
|
|
431
|
+
|
|
432
|
+
print("Gaia assistant doctor")
|
|
433
|
+
print("====================")
|
|
434
|
+
|
|
435
|
+
for name in required_cmds:
|
|
436
|
+
if shutil.which(name):
|
|
437
|
+
print(f"[ok] required command found: {name}")
|
|
438
|
+
else:
|
|
439
|
+
problems += 1
|
|
440
|
+
print(f"[missing] required command not found: {name}")
|
|
441
|
+
|
|
442
|
+
for name in optional_cmds:
|
|
443
|
+
if shutil.which(name):
|
|
444
|
+
print(f"[ok] optional command found: {name}")
|
|
445
|
+
else:
|
|
446
|
+
print(f"[warn] optional command not found: {name}")
|
|
447
|
+
|
|
448
|
+
anth = os.environ.get("ANTHROPIC_API_KEY")
|
|
449
|
+
oai = os.environ.get("OPENAI_API_KEY")
|
|
450
|
+
if anth or oai:
|
|
451
|
+
print("[ok] at least one API auth env var is present")
|
|
452
|
+
else:
|
|
453
|
+
print("[warn] no API key env vars found (ANTHROPIC_API_KEY / OPENAI_API_KEY)")
|
|
454
|
+
print(" subscription OAuth profiles may still be used depending on your runtime setup")
|
|
455
|
+
|
|
456
|
+
cfg = _load_json(cfg_path)
|
|
457
|
+
if not cfg:
|
|
458
|
+
print(f"[warn] launcher config not found yet: {cfg_path}")
|
|
459
|
+
launcher = _launcher_hint()
|
|
460
|
+
print(f" run `{launcher} init` or `{launcher} onboard`")
|
|
461
|
+
else:
|
|
462
|
+
cfg = _normalize_config(cfg)
|
|
463
|
+
active = cfg.get("auth", {}).get("active_profile")
|
|
464
|
+
if not isinstance(active, dict):
|
|
465
|
+
print("[warn] no linked OAuth profile in launcher config")
|
|
466
|
+
print(f" run `{_launcher_hint()} onboard`")
|
|
467
|
+
else:
|
|
468
|
+
credential, error = _read_linked_credential(active)
|
|
469
|
+
if credential is None:
|
|
470
|
+
print(f"[warn] {error}")
|
|
471
|
+
else:
|
|
472
|
+
profile_id = str(active.get("profile_id", "")).strip()
|
|
473
|
+
expiry = _format_expiry(credential)
|
|
474
|
+
if _is_expired(credential):
|
|
475
|
+
print(f"[warn] linked OAuth profile is expired: {profile_id} (expires={expiry})")
|
|
476
|
+
else:
|
|
477
|
+
print(f"[ok] linked OAuth profile found: {profile_id} (expires={expiry})")
|
|
478
|
+
|
|
479
|
+
if not AGENT_CONFIG_PATH.exists():
|
|
480
|
+
problems += 1
|
|
481
|
+
print(f"[missing] agent config not found: {AGENT_CONFIG_PATH}")
|
|
482
|
+
else:
|
|
483
|
+
print(f"[ok] agent config found: {AGENT_CONFIG_PATH}")
|
|
484
|
+
|
|
485
|
+
if not AGENT_LOOP_PATH.exists():
|
|
486
|
+
problems += 1
|
|
487
|
+
print(f"[missing] agent loop not found: {AGENT_LOOP_PATH}")
|
|
488
|
+
else:
|
|
489
|
+
print(f"[ok] agent loop found: {AGENT_LOOP_PATH}")
|
|
490
|
+
|
|
491
|
+
if problems:
|
|
492
|
+
print(f"\nDoctor found {problems} blocking problem(s).", file=sys.stderr)
|
|
493
|
+
return 1
|
|
494
|
+
print("\nDoctor finished with no blocking problems.")
|
|
495
|
+
return 0
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def cmd_onboard(args: argparse.Namespace) -> int:
|
|
499
|
+
cfg_path = Path(args.config).expanduser()
|
|
500
|
+
state_dir = Path(args.state_dir).expanduser()
|
|
501
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
502
|
+
cfg_created = not cfg_path.exists()
|
|
503
|
+
_ensure_config_exists(cfg_path)
|
|
504
|
+
|
|
505
|
+
print("Gaia assistant onboarding")
|
|
506
|
+
print("=========================")
|
|
507
|
+
if cfg_created:
|
|
508
|
+
print(f"[ok] created launcher config: {cfg_path}")
|
|
509
|
+
else:
|
|
510
|
+
print(f"[ok] using existing launcher config: {cfg_path}")
|
|
511
|
+
print(f"[ok] state directory ready: {state_dir}")
|
|
512
|
+
print("")
|
|
513
|
+
print("Auth flow selected: Gaia native profile store + Codex web OAuth broker")
|
|
514
|
+
print("This opens a browser/device auth flow through Codex CLI.")
|
|
515
|
+
print("Tokens are copied into Gaia local auth store (outside this repository).")
|
|
516
|
+
sys.stdout.flush()
|
|
517
|
+
|
|
518
|
+
proceed = "y"
|
|
519
|
+
if not args.yes:
|
|
520
|
+
proceed = input("Start web OAuth login now? [Y/n]: ").strip().lower()
|
|
521
|
+
if proceed in ("n", "no"):
|
|
522
|
+
print("Skipped OAuth login. Run this later:")
|
|
523
|
+
print(f" {_launcher_hint()} auth login --provider openai-codex")
|
|
524
|
+
return 0
|
|
525
|
+
|
|
526
|
+
login_args = argparse.Namespace(
|
|
527
|
+
config=str(cfg_path),
|
|
528
|
+
provider="openai-codex",
|
|
529
|
+
source="codex-cli",
|
|
530
|
+
codex_auth_path=args.codex_auth_path,
|
|
531
|
+
gaia_auth_store=args.gaia_auth_store,
|
|
532
|
+
openclaw_agent="main",
|
|
533
|
+
openclaw_auth_store=None,
|
|
534
|
+
no_prompt=True,
|
|
535
|
+
)
|
|
536
|
+
return cmd_auth_login(login_args)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def cmd_auth_login(args: argparse.Namespace) -> int:
|
|
540
|
+
cfg_path = Path(args.config).expanduser()
|
|
541
|
+
_ensure_config_exists(cfg_path)
|
|
542
|
+
provider = str(args.provider).strip()
|
|
543
|
+
source = str(args.source).strip()
|
|
544
|
+
|
|
545
|
+
if provider != "openai-codex":
|
|
546
|
+
print(f"Unsupported OAuth provider: {provider}", file=sys.stderr)
|
|
547
|
+
print("Supported provider for now: openai-codex", file=sys.stderr)
|
|
548
|
+
return 1
|
|
549
|
+
|
|
550
|
+
if source == "codex-cli":
|
|
551
|
+
if not args.no_prompt:
|
|
552
|
+
answer = input(
|
|
553
|
+
"Gaia will run Codex web OAuth login, then import credentials into Gaia local auth store.\n"
|
|
554
|
+
"Continue? [Y/n]: "
|
|
555
|
+
).strip().lower()
|
|
556
|
+
if answer in ("n", "no"):
|
|
557
|
+
print("Canceled.")
|
|
558
|
+
return 1
|
|
559
|
+
|
|
560
|
+
rc = _codex_login()
|
|
561
|
+
if rc != 0:
|
|
562
|
+
return rc
|
|
563
|
+
|
|
564
|
+
codex_auth_path = _resolve_codex_auth_path(args.codex_auth_path)
|
|
565
|
+
gaia_auth_store = _resolve_gaia_auth_store(cfg_path, args.gaia_auth_store)
|
|
566
|
+
rc, profile_id = _import_codex_profile_to_gaia(cfg_path, provider, codex_auth_path, gaia_auth_store)
|
|
567
|
+
if rc != 0:
|
|
568
|
+
return rc
|
|
569
|
+
|
|
570
|
+
store = _load_gaia_auth_store(gaia_auth_store)
|
|
571
|
+
profiles = store.get("profiles", {}) if isinstance(store, dict) else {}
|
|
572
|
+
credential = profiles.get(profile_id) if isinstance(profiles, dict) else {}
|
|
573
|
+
expiry = _format_expiry(credential if isinstance(credential, dict) else {})
|
|
574
|
+
|
|
575
|
+
print("[ok] OAuth profile linked for Gaia assistant")
|
|
576
|
+
print(f" source: gaia-local (imported from codex-cli)")
|
|
577
|
+
print(f" provider: {provider}")
|
|
578
|
+
print(f" profile: {profile_id}")
|
|
579
|
+
print(f" store: {gaia_auth_store}")
|
|
580
|
+
print(f" expires: {expiry}")
|
|
581
|
+
print("")
|
|
582
|
+
print("Note: credentials are stored in local Gaia auth store, not in this repository.")
|
|
583
|
+
return 0
|
|
584
|
+
|
|
585
|
+
if source == "openclaw":
|
|
586
|
+
if not args.no_prompt:
|
|
587
|
+
answer = input(
|
|
588
|
+
"OAuth login opens a browser and stores tokens in OpenClaw local state.\n"
|
|
589
|
+
"Continue? [Y/n]: "
|
|
590
|
+
).strip().lower()
|
|
591
|
+
if answer in ("n", "no"):
|
|
592
|
+
print("Canceled.")
|
|
593
|
+
return 1
|
|
594
|
+
|
|
595
|
+
if not shutil.which("openclaw"):
|
|
596
|
+
print(
|
|
597
|
+
"OpenClaw CLI is not installed. Install it, or use --source codex-cli.",
|
|
598
|
+
file=sys.stderr,
|
|
599
|
+
)
|
|
600
|
+
return 1
|
|
601
|
+
|
|
602
|
+
rc = subprocess.run(["openclaw", "models", "auth", "login", "--provider", provider]).returncode
|
|
603
|
+
if rc != 0:
|
|
604
|
+
return rc
|
|
605
|
+
|
|
606
|
+
store_path = _resolve_openclaw_auth_store(args.openclaw_agent, args.openclaw_auth_store)
|
|
607
|
+
profiles = _load_openclaw_profiles(store_path)
|
|
608
|
+
provider_profiles = _collect_provider_profiles(profiles, provider)
|
|
609
|
+
if not provider_profiles:
|
|
610
|
+
print(
|
|
611
|
+
"OAuth login finished but no matching OpenClaw profile was found.\n"
|
|
612
|
+
f"Expected store: {store_path}",
|
|
613
|
+
file=sys.stderr,
|
|
614
|
+
)
|
|
615
|
+
return 1
|
|
616
|
+
|
|
617
|
+
selected_profile_id = args.profile_id or _pick_profile_id(provider_profiles)
|
|
618
|
+
if selected_profile_id not in provider_profiles:
|
|
619
|
+
print(
|
|
620
|
+
f"Requested profile not found for provider {provider}: {selected_profile_id}",
|
|
621
|
+
file=sys.stderr,
|
|
622
|
+
)
|
|
623
|
+
return 1
|
|
624
|
+
|
|
625
|
+
_link_openclaw_profile(cfg_path, provider, selected_profile_id, store_path)
|
|
626
|
+
credential = provider_profiles[selected_profile_id]
|
|
627
|
+
print("[ok] OAuth profile linked for Gaia assistant")
|
|
628
|
+
print(f" source: openclaw")
|
|
629
|
+
print(f" provider: {provider}")
|
|
630
|
+
print(f" profile: {selected_profile_id}")
|
|
631
|
+
print(f" store: {store_path}")
|
|
632
|
+
print(f" expires: {_format_expiry(credential)}")
|
|
633
|
+
print("")
|
|
634
|
+
print("Note: tokens are not written to this repository.")
|
|
635
|
+
return 0
|
|
636
|
+
|
|
637
|
+
print(f"Unsupported auth source: {source}", file=sys.stderr)
|
|
638
|
+
print("Supported sources: codex-cli, openclaw", file=sys.stderr)
|
|
639
|
+
return 1
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def cmd_auth_link(args: argparse.Namespace) -> int:
|
|
643
|
+
cfg_path = Path(args.config).expanduser()
|
|
644
|
+
_ensure_config_exists(cfg_path)
|
|
645
|
+
provider = str(args.provider).strip()
|
|
646
|
+
source = str(args.source).strip()
|
|
647
|
+
|
|
648
|
+
if source == "codex-cli":
|
|
649
|
+
codex_auth_path = _resolve_codex_auth_path(args.codex_auth_path)
|
|
650
|
+
gaia_auth_store = _resolve_gaia_auth_store(cfg_path, args.gaia_auth_store)
|
|
651
|
+
rc, profile_id = _import_codex_profile_to_gaia(cfg_path, provider, codex_auth_path, gaia_auth_store)
|
|
652
|
+
if rc != 0:
|
|
653
|
+
return rc
|
|
654
|
+
print("[ok] Imported and linked Codex OAuth profile into Gaia local store")
|
|
655
|
+
print(f" provider: {provider}")
|
|
656
|
+
print(f" profile: {profile_id}")
|
|
657
|
+
print(f" store: {gaia_auth_store}")
|
|
658
|
+
return 0
|
|
659
|
+
|
|
660
|
+
if source == "openclaw":
|
|
661
|
+
store_path = _resolve_openclaw_auth_store(args.openclaw_agent, args.openclaw_auth_store)
|
|
662
|
+
profiles = _load_openclaw_profiles(store_path)
|
|
663
|
+
provider_profiles = _collect_provider_profiles(profiles, provider)
|
|
664
|
+
if not provider_profiles:
|
|
665
|
+
print(
|
|
666
|
+
f"No profiles found for provider '{provider}' in {store_path}.",
|
|
667
|
+
file=sys.stderr,
|
|
668
|
+
)
|
|
669
|
+
return 1
|
|
670
|
+
|
|
671
|
+
selected_profile_id = args.profile_id or _pick_profile_id(provider_profiles)
|
|
672
|
+
if selected_profile_id not in provider_profiles:
|
|
673
|
+
print(f"Requested profile not found: {selected_profile_id}", file=sys.stderr)
|
|
674
|
+
return 1
|
|
675
|
+
|
|
676
|
+
_link_openclaw_profile(cfg_path, provider, selected_profile_id, store_path)
|
|
677
|
+
print("[ok] Linked existing OpenClaw auth profile")
|
|
678
|
+
print(f" provider: {provider}")
|
|
679
|
+
print(f" profile: {selected_profile_id}")
|
|
680
|
+
print(f" store: {store_path}")
|
|
681
|
+
return 0
|
|
682
|
+
|
|
683
|
+
print(f"Unsupported auth source: {source}", file=sys.stderr)
|
|
684
|
+
print("Supported sources: codex-cli, openclaw", file=sys.stderr)
|
|
685
|
+
return 1
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def cmd_auth_status(args: argparse.Namespace) -> int:
|
|
689
|
+
cfg_path = Path(args.config).expanduser()
|
|
690
|
+
cfg = _load_json(cfg_path)
|
|
691
|
+
if not cfg:
|
|
692
|
+
print(f"Launcher config not found: {cfg_path}")
|
|
693
|
+
return 1
|
|
694
|
+
|
|
695
|
+
cfg = _normalize_config(cfg)
|
|
696
|
+
active = cfg.get("auth", {}).get("active_profile")
|
|
697
|
+
if not isinstance(active, dict):
|
|
698
|
+
print("No linked auth profile in launcher config.")
|
|
699
|
+
print(f"Run: {_launcher_hint()} auth login --provider openai-codex")
|
|
700
|
+
return 1
|
|
701
|
+
|
|
702
|
+
provider = str(active.get("provider", "")).strip()
|
|
703
|
+
profile_id = str(active.get("profile_id", "")).strip()
|
|
704
|
+
source = str(active.get("source", "")).strip()
|
|
705
|
+
store_path = Path(str(active.get("store_path", "")).strip()).expanduser()
|
|
706
|
+
|
|
707
|
+
print("Gaia assistant auth status")
|
|
708
|
+
print("==========================")
|
|
709
|
+
print(f"source: {source}")
|
|
710
|
+
print(f"provider: {provider}")
|
|
711
|
+
print(f"profile: {profile_id}")
|
|
712
|
+
print(f"store: {store_path}")
|
|
713
|
+
|
|
714
|
+
credential, error = _read_linked_credential(active)
|
|
715
|
+
if credential is None:
|
|
716
|
+
print(f"[warn] {error}")
|
|
717
|
+
return 1
|
|
718
|
+
|
|
719
|
+
cred_type = str(credential.get("type", "unknown"))
|
|
720
|
+
expires = _format_expiry(credential)
|
|
721
|
+
email = str(credential.get("email", "")).strip() or "n/a"
|
|
722
|
+
print(f"type: {cred_type}")
|
|
723
|
+
print(f"email: {email}")
|
|
724
|
+
print(f"expires: {expires}")
|
|
725
|
+
if _is_expired(credential):
|
|
726
|
+
print("[warn] OAuth profile is expired")
|
|
727
|
+
return 1
|
|
728
|
+
print("[ok] OAuth profile is ready")
|
|
729
|
+
return 0
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
733
|
+
cfg_path = Path(args.config).expanduser()
|
|
734
|
+
try:
|
|
735
|
+
cfg = _ensure_config_exists(cfg_path)
|
|
736
|
+
except PermissionError:
|
|
737
|
+
print(
|
|
738
|
+
"Cannot create runtime config due to permissions. "
|
|
739
|
+
"Set GAIA_ASSISTANT_HOME or pass --config to a writable path.",
|
|
740
|
+
file=sys.stderr,
|
|
741
|
+
)
|
|
742
|
+
return 1
|
|
743
|
+
|
|
744
|
+
cmd = [
|
|
745
|
+
"python3",
|
|
746
|
+
str(AGENT_LOOP_PATH),
|
|
747
|
+
"--config",
|
|
748
|
+
str(AGENT_CONFIG_PATH),
|
|
749
|
+
"--mode",
|
|
750
|
+
args.mode,
|
|
751
|
+
]
|
|
752
|
+
if args.dry_run:
|
|
753
|
+
cmd.append("--dry-run")
|
|
754
|
+
if args.verbose:
|
|
755
|
+
cmd.append("--verbose")
|
|
756
|
+
|
|
757
|
+
env = os.environ.copy()
|
|
758
|
+
if args.track != "auto":
|
|
759
|
+
env["GAIA_ACTIVE_TRACK_OVERRIDE"] = args.track
|
|
760
|
+
|
|
761
|
+
runtime_cfg = cfg.get("runtime", {})
|
|
762
|
+
if args.mode == "continuous" and "interval_minutes" in runtime_cfg:
|
|
763
|
+
print(f"Running Gaia assistant in continuous mode (interval={runtime_cfg['interval_minutes']}m)")
|
|
764
|
+
else:
|
|
765
|
+
print(f"Running Gaia assistant in {args.mode} mode")
|
|
766
|
+
if args.track != "auto":
|
|
767
|
+
print(f"Track override: {args.track}")
|
|
768
|
+
|
|
769
|
+
active_profile = cfg.get("auth", {}).get("active_profile")
|
|
770
|
+
if isinstance(active_profile, dict):
|
|
771
|
+
print(
|
|
772
|
+
"Auth profile: "
|
|
773
|
+
f"{active_profile.get('provider', '?')}/{active_profile.get('profile_id', '?')} "
|
|
774
|
+
f"({active_profile.get('source', '?')})"
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
result = subprocess.run(cmd, cwd=str(REPO_ROOT), env=env)
|
|
778
|
+
return result.returncode
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
782
|
+
parser = argparse.ArgumentParser(
|
|
783
|
+
description="Gaia standalone personal assistant launcher",
|
|
784
|
+
)
|
|
785
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
786
|
+
|
|
787
|
+
init = sub.add_parser("init", help="Initialize local Gaia assistant config")
|
|
788
|
+
init.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Config JSON path")
|
|
789
|
+
init.add_argument("--state-dir", default=str(DEFAULT_STATE_DIR), help="Local state directory")
|
|
790
|
+
init.add_argument("--force", action="store_true", help="Overwrite config if it exists")
|
|
791
|
+
init.set_defaults(func=cmd_init)
|
|
792
|
+
|
|
793
|
+
onboard = sub.add_parser("onboard", help="Guided onboarding with Gaia-native OAuth linking")
|
|
794
|
+
onboard.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Config JSON path")
|
|
795
|
+
onboard.add_argument("--state-dir", default=str(DEFAULT_STATE_DIR), help="Local state directory")
|
|
796
|
+
onboard.add_argument(
|
|
797
|
+
"--gaia-auth-store",
|
|
798
|
+
default=None,
|
|
799
|
+
help="Path to Gaia auth-profiles.json (optional override)",
|
|
800
|
+
)
|
|
801
|
+
onboard.add_argument(
|
|
802
|
+
"--codex-auth-path",
|
|
803
|
+
default=None,
|
|
804
|
+
help="Path to Codex auth.json (optional override)",
|
|
805
|
+
)
|
|
806
|
+
onboard.add_argument("--yes", action="store_true", help="Skip onboarding confirmations")
|
|
807
|
+
onboard.set_defaults(func=cmd_onboard)
|
|
808
|
+
|
|
809
|
+
doctor = sub.add_parser("doctor", help="Validate local environment and auth readiness")
|
|
810
|
+
doctor.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Config JSON path")
|
|
811
|
+
doctor.set_defaults(func=cmd_doctor)
|
|
812
|
+
|
|
813
|
+
auth = sub.add_parser("auth", help="Manage OAuth profile linkage for Gaia assistant")
|
|
814
|
+
auth_sub = auth.add_subparsers(dest="auth_command", required=True)
|
|
815
|
+
|
|
816
|
+
auth_login = auth_sub.add_parser("login", help="Run web OAuth login and link profile")
|
|
817
|
+
auth_login.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Config JSON path")
|
|
818
|
+
auth_login.add_argument("--provider", default="openai-codex", help="Provider id (default: openai-codex)")
|
|
819
|
+
auth_login.add_argument(
|
|
820
|
+
"--source",
|
|
821
|
+
choices=["codex-cli", "openclaw"],
|
|
822
|
+
default="codex-cli",
|
|
823
|
+
help="OAuth source (default: codex-cli)",
|
|
824
|
+
)
|
|
825
|
+
auth_login.add_argument(
|
|
826
|
+
"--gaia-auth-store",
|
|
827
|
+
default=None,
|
|
828
|
+
help="Path to Gaia auth-profiles.json (optional override)",
|
|
829
|
+
)
|
|
830
|
+
auth_login.add_argument(
|
|
831
|
+
"--codex-auth-path",
|
|
832
|
+
default=None,
|
|
833
|
+
help="Path to Codex auth.json (optional override)",
|
|
834
|
+
)
|
|
835
|
+
auth_login.add_argument("--openclaw-agent", default="main", help="OpenClaw agent id (openclaw source only)")
|
|
836
|
+
auth_login.add_argument(
|
|
837
|
+
"--openclaw-auth-store",
|
|
838
|
+
default=None,
|
|
839
|
+
help="Path to OpenClaw auth-profiles.json (openclaw source only)",
|
|
840
|
+
)
|
|
841
|
+
auth_login.add_argument("--profile-id", default=None, help="Explicit profile id to link")
|
|
842
|
+
auth_login.add_argument("--no-prompt", action="store_true", help="Skip confirmation prompts")
|
|
843
|
+
auth_login.set_defaults(func=cmd_auth_login)
|
|
844
|
+
|
|
845
|
+
auth_link = auth_sub.add_parser("link", help="Link an existing profile without logging in")
|
|
846
|
+
auth_link.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Config JSON path")
|
|
847
|
+
auth_link.add_argument("--provider", default="openai-codex", help="Provider id (default: openai-codex)")
|
|
848
|
+
auth_link.add_argument(
|
|
849
|
+
"--source",
|
|
850
|
+
choices=["codex-cli", "openclaw"],
|
|
851
|
+
default="codex-cli",
|
|
852
|
+
help="Profile source (default: codex-cli)",
|
|
853
|
+
)
|
|
854
|
+
auth_link.add_argument(
|
|
855
|
+
"--gaia-auth-store",
|
|
856
|
+
default=None,
|
|
857
|
+
help="Path to Gaia auth-profiles.json (optional override)",
|
|
858
|
+
)
|
|
859
|
+
auth_link.add_argument(
|
|
860
|
+
"--codex-auth-path",
|
|
861
|
+
default=None,
|
|
862
|
+
help="Path to Codex auth.json (optional override)",
|
|
863
|
+
)
|
|
864
|
+
auth_link.add_argument("--openclaw-agent", default="main", help="OpenClaw agent id (openclaw source only)")
|
|
865
|
+
auth_link.add_argument(
|
|
866
|
+
"--openclaw-auth-store",
|
|
867
|
+
default=None,
|
|
868
|
+
help="Path to OpenClaw auth-profiles.json (openclaw source only)",
|
|
869
|
+
)
|
|
870
|
+
auth_link.add_argument("--profile-id", default=None, help="Explicit profile id to link")
|
|
871
|
+
auth_link.set_defaults(func=cmd_auth_link)
|
|
872
|
+
|
|
873
|
+
auth_status = auth_sub.add_parser("status", help="Show linked auth profile status")
|
|
874
|
+
auth_status.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Config JSON path")
|
|
875
|
+
auth_status.set_defaults(func=cmd_auth_status)
|
|
876
|
+
|
|
877
|
+
run = sub.add_parser("run", help="Run Gaia assistant loop")
|
|
878
|
+
run.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Launcher config JSON path")
|
|
879
|
+
run.add_argument("--mode", choices=["single", "continuous"], default="single")
|
|
880
|
+
run.add_argument("--track", choices=["auto", "assistant", "framework"], default="auto")
|
|
881
|
+
run.add_argument("--dry-run", action="store_true", help="Plan only, do not execute actions")
|
|
882
|
+
run.add_argument("--verbose", action="store_true", help="Verbose logs")
|
|
883
|
+
run.set_defaults(func=cmd_run)
|
|
884
|
+
|
|
885
|
+
return parser
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def main() -> int:
|
|
889
|
+
parser = build_parser()
|
|
890
|
+
args = parser.parse_args()
|
|
891
|
+
return int(args.func(args))
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
if __name__ == "__main__":
|
|
895
|
+
raise SystemExit(main())
|