@0dai-dev/cli 4.3.6 → 4.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/bin/0dai.js +133 -33
- package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
- package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
- package/lib/ai/registry/mcp-catalog.json +98 -0
- package/lib/commands/auth.js +2 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/doctor.js +707 -12
- package/lib/commands/experience.js +40 -5
- package/lib/commands/feedback.js +157 -15
- package/lib/commands/gh.js +26 -0
- package/lib/commands/graph.js +9 -4
- package/lib/commands/heatmap.js +1 -1
- package/lib/commands/init.js +298 -27
- package/lib/commands/mcp.js +111 -33
- package/lib/commands/models.js +138 -41
- package/lib/commands/play.js +20 -4
- package/lib/commands/provider.js +30 -59
- package/lib/commands/quota.js +1 -1
- package/lib/commands/receipt.js +1 -1
- package/lib/commands/run.js +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +176 -11
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/trust.js +1 -1
- package/lib/commands/update.js +184 -38
- package/lib/commands/usage.js +1 -1
- package/lib/commands/validate.js +32 -3
- package/lib/commands/vault.js +43 -8
- package/lib/python/__init__.py +0 -0
- package/lib/python/agent_quotas.py +525 -0
- package/lib/python/anomaly_alert.py +397 -0
- package/lib/python/anti_pattern_detector.py +799 -0
- package/lib/python/auth.py +443 -0
- package/lib/python/capi_profile_guard.py +477 -0
- package/lib/python/compliance_report.py +581 -0
- package/lib/python/drift_detector.py +388 -0
- package/lib/python/experience_pipeline.py +1130 -0
- package/lib/python/graph.py +19 -0
- package/lib/python/graph_core.py +293 -0
- package/lib/python/graph_io.py +179 -0
- package/lib/python/graph_legacy.py +2052 -0
- package/lib/python/graph_legacy_helpers.py +221 -0
- package/lib/python/graph_outcomes_core.py +85 -0
- package/lib/python/graph_queries.py +171 -0
- package/lib/python/graph_slice.py +198 -0
- package/lib/python/graph_slicer.py +576 -0
- package/lib/python/graph_slicer_cli.py +60 -0
- package/lib/python/graph_validation.py +64 -0
- package/lib/python/heatmap.py +943 -0
- package/lib/python/json_utils.py +193 -0
- package/lib/python/mcp_exposure_check.py +247 -0
- package/lib/python/model_router.py +1434 -0
- package/lib/python/project_manager.py +621 -0
- package/lib/python/provider_profiles.py +1618 -0
- package/lib/python/provider_registry.py +1211 -0
- package/lib/python/provider_registry_cli.py +125 -0
- package/lib/python/receipt_png.py +727 -0
- package/lib/python/structural_memory.py +325 -0
- package/lib/python/swarm_cost.py +177 -0
- package/lib/python/usage_ledger.py +569 -0
- package/lib/scripts/mcp_tier_config.py +240 -0
- package/lib/shared.js +96 -12
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +1 -4
- package/lib/utils/constants.js +7 -1
- package/lib/utils/identity.js +184 -0
- package/lib/utils/mcp-auth.js +81 -15
- package/lib/utils/plan.js +1 -1
- package/lib/vault/index.js +19 -3
- package/lib/vault/storage.js +21 -2
- package/lib/wizard.js +5 -2
- package/package.json +9 -3
- package/scripts/build-python-bundle.js +106 -0
- package/scripts/build-tui.js +14 -1
- package/scripts/harvest_experience.py +523 -0
- package/scripts/postinstall.js +15 -9
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""0dai Auth — cloud authentication for team and enterprise features.
|
|
3
|
+
|
|
4
|
+
Core CLI works without auth. Team features require `0dai auth login`.
|
|
5
|
+
Token stored locally at ~/.0dai/auth.json, refreshes weekly.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
0dai auth login # authenticate via browser
|
|
9
|
+
0dai auth login --code XXXX-YYYY # paste code directly
|
|
10
|
+
0dai auth signup --email <e> # create account
|
|
11
|
+
0dai auth status # show current auth state
|
|
12
|
+
0dai auth refresh # refresh token manually
|
|
13
|
+
0dai auth logout # remove token
|
|
14
|
+
0dai auth --json # JSON output
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
import urllib.request
|
|
25
|
+
import urllib.error
|
|
26
|
+
|
|
27
|
+
AUTH_DIR = pathlib.Path.home() / ".0dai"
|
|
28
|
+
TOKEN_PATH = AUTH_DIR / "auth.json"
|
|
29
|
+
TOKEN_TTL_SECONDS = 7 * 24 * 3600 # 7 days offline
|
|
30
|
+
API_BASE = os.environ.get("ODAI_API_URL", "https://api.0dai.dev")
|
|
31
|
+
GOTRUE_URL = os.environ.get("ODAI_GOTRUE_URL", "http://localhost:9999")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now_iso() -> str:
|
|
35
|
+
return time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _now_ts() -> float:
|
|
39
|
+
return time.time()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _api_post_status(endpoint: str, payload: dict, timeout: int = 15) -> tuple[dict | None, bool]:
|
|
43
|
+
"""POST to cloud API. Returns ``(response|None, reached)``.
|
|
44
|
+
|
|
45
|
+
``reached`` is True when the server answered at all — including an HTTP
|
|
46
|
+
error status (a rejection) or an unparseable body — and False only when
|
|
47
|
+
the server was unreachable (DNS/connection/timeout). Callers must not
|
|
48
|
+
treat a rejection as "offline" (#4363).
|
|
49
|
+
"""
|
|
50
|
+
url = f"{API_BASE}{endpoint}"
|
|
51
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
52
|
+
req = urllib.request.Request(
|
|
53
|
+
url, data=body,
|
|
54
|
+
headers={"Content-Type": "application/json", "User-Agent": "0dai-cli/0.9"},
|
|
55
|
+
method="POST",
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
59
|
+
return json.loads(resp.read().decode("utf-8")), True
|
|
60
|
+
except urllib.error.HTTPError:
|
|
61
|
+
return None, True # server reached and rejected the request
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
return None, True # server answered, just not parseable
|
|
64
|
+
except (urllib.error.URLError, OSError):
|
|
65
|
+
return None, False # unreachable
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _api_post(endpoint: str, payload: dict, timeout: int = 15) -> dict | None:
|
|
69
|
+
"""POST to cloud API. Returns response dict or None on failure."""
|
|
70
|
+
result, _reached = _api_post_status(endpoint, payload, timeout=timeout)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_token() -> dict | None:
|
|
75
|
+
if not TOKEN_PATH.is_file():
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
return json.loads(TOKEN_PATH.read_text(encoding="utf-8"))
|
|
79
|
+
except (json.JSONDecodeError, OSError):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save_token(token: dict) -> None:
|
|
84
|
+
AUTH_DIR.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
TOKEN_PATH.write_text(json.dumps(token, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
86
|
+
try:
|
|
87
|
+
TOKEN_PATH.chmod(0o600)
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def remove_token() -> None:
|
|
93
|
+
if TOKEN_PATH.is_file():
|
|
94
|
+
TOKEN_PATH.unlink()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_authenticated() -> bool:
|
|
98
|
+
token = load_token()
|
|
99
|
+
if not token:
|
|
100
|
+
return False
|
|
101
|
+
expires = token.get("expires_at", 0)
|
|
102
|
+
if isinstance(expires, str):
|
|
103
|
+
try:
|
|
104
|
+
import datetime
|
|
105
|
+
dt = datetime.datetime.fromisoformat(expires.replace("+00:00", "+00:00"))
|
|
106
|
+
expires = dt.timestamp()
|
|
107
|
+
except (ValueError, AttributeError):
|
|
108
|
+
return False
|
|
109
|
+
return _now_ts() < expires
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_plan() -> str:
|
|
113
|
+
token = load_token()
|
|
114
|
+
if not token or not is_authenticated():
|
|
115
|
+
return "free"
|
|
116
|
+
return token.get("plan", "free")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def check_cloud_tier(required: str) -> bool:
|
|
120
|
+
if required == "free":
|
|
121
|
+
return True
|
|
122
|
+
if not is_authenticated():
|
|
123
|
+
return False
|
|
124
|
+
token = load_token()
|
|
125
|
+
if not token:
|
|
126
|
+
return False
|
|
127
|
+
current = token.get("plan", "free")
|
|
128
|
+
levels = {"free": 0, "team": 1, "enterprise": 2}
|
|
129
|
+
return levels.get(current, 0) >= levels.get(required, 0)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_code(code: str) -> dict | None:
|
|
133
|
+
"""Validate auth code against cloud API with local fallback."""
|
|
134
|
+
code = code.strip().upper()
|
|
135
|
+
parts = code.split("-")
|
|
136
|
+
if len(parts) != 2 or len(parts[0]) != 4 or len(parts[1]) != 4:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# Ask the server. A reachable server that does not validate the code is an
|
|
140
|
+
# explicit rejection — never fall through to an offline grant on a rejected
|
|
141
|
+
# code (that let any well-formed XXXX-YYYY mint team/enterprise) (#4363).
|
|
142
|
+
api_result, reached = _api_post_status("/auth/validate", {"code": code})
|
|
143
|
+
if api_result and api_result.get("token_id"):
|
|
144
|
+
api_result["expires_at"] = api_result.get("expires_at", time.strftime(
|
|
145
|
+
"%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(_now_ts() + TOKEN_TTL_SECONDS),
|
|
146
|
+
))
|
|
147
|
+
return api_result
|
|
148
|
+
if reached:
|
|
149
|
+
return None # server answered and did not validate the code
|
|
150
|
+
|
|
151
|
+
# Server unreachable: an offline grant is dev-only and opt-in (it confers a
|
|
152
|
+
# plan from the code prefix with no server trust, so it must never be the
|
|
153
|
+
# default for a normal user). Gate it behind ODAI_AUTH_OFFLINE_DEV=1.
|
|
154
|
+
if os.environ.get("ODAI_AUTH_OFFLINE_DEV", "").strip().lower() not in {"1", "true", "yes", "on"}:
|
|
155
|
+
return None
|
|
156
|
+
code_hash = hashlib.sha256(code.encode()).hexdigest()[:12]
|
|
157
|
+
plan = "enterprise" if code.startswith("ENT") else "team"
|
|
158
|
+
return {
|
|
159
|
+
"token_id": f"tok_{code_hash}",
|
|
160
|
+
"user": os.environ.get("USER", "dev") + "@local",
|
|
161
|
+
"team": "Local" if plan == "team" else "Enterprise Local",
|
|
162
|
+
"plan": plan,
|
|
163
|
+
"seats": 999 if plan == "enterprise" else 10,
|
|
164
|
+
"authenticated_at": _now_iso(),
|
|
165
|
+
"expires_at": time.strftime(
|
|
166
|
+
"%Y-%m-%dT%H:%M:%S+00:00",
|
|
167
|
+
time.gmtime(_now_ts() + TOKEN_TTL_SECONDS),
|
|
168
|
+
),
|
|
169
|
+
"refresh_url": f"{API_BASE}/auth/refresh",
|
|
170
|
+
"mode": "offline",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _gotrue_post(endpoint: str, payload: dict, timeout: int = 15) -> dict | None:
|
|
175
|
+
"""POST to GoTrue auth server."""
|
|
176
|
+
url = f"{GOTRUE_URL}{endpoint}"
|
|
177
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
178
|
+
req = urllib.request.Request(
|
|
179
|
+
url, data=body,
|
|
180
|
+
headers={"Content-Type": "application/json", "User-Agent": "0dai-cli/0.9"},
|
|
181
|
+
method="POST",
|
|
182
|
+
)
|
|
183
|
+
try:
|
|
184
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
185
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
186
|
+
except (urllib.error.URLError, json.JSONDecodeError, OSError):
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _signup(email: str, password: str = "") -> dict | None:
|
|
191
|
+
"""Register via GoTrue, with offline fallback."""
|
|
192
|
+
if not password:
|
|
193
|
+
password = hashlib.sha256(email.encode()).hexdigest()[:16]
|
|
194
|
+
|
|
195
|
+
# Try real GoTrue signup
|
|
196
|
+
result = _gotrue_post("/signup", {"email": email, "password": password})
|
|
197
|
+
if result and result.get("access_token"):
|
|
198
|
+
user = result.get("user", {})
|
|
199
|
+
code_hash = hashlib.sha256(email.encode()).hexdigest()[:8]
|
|
200
|
+
auth_code = f"{code_hash[:4].upper()}-{code_hash[4:8].upper()}"
|
|
201
|
+
return {
|
|
202
|
+
"success": True,
|
|
203
|
+
"user": email,
|
|
204
|
+
"user_id": user.get("id", ""),
|
|
205
|
+
"team": email.split("@", maxsplit=1)[0] + "-team",
|
|
206
|
+
"auth_code": auth_code,
|
|
207
|
+
"access_token": result["access_token"],
|
|
208
|
+
"refresh_token": result.get("refresh_token", ""),
|
|
209
|
+
"message": "Account created via GoTrue.",
|
|
210
|
+
"mode": "gotrue",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Offline fallback
|
|
214
|
+
code_hash = hashlib.sha256(email.encode()).hexdigest()[:8]
|
|
215
|
+
return {
|
|
216
|
+
"success": True,
|
|
217
|
+
"user": email,
|
|
218
|
+
"team": email.split("@", maxsplit=1)[0] + "-team",
|
|
219
|
+
"auth_code": f"{code_hash[:4].upper()}-{code_hash[4:8].upper()}",
|
|
220
|
+
"message": "Account created (offline mode). Use auth code to login.",
|
|
221
|
+
"mode": "offline",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _refresh_token(token: dict) -> dict | None:
|
|
226
|
+
"""Refresh token via GoTrue or extend TTL offline."""
|
|
227
|
+
refresh_tok = token.get("refresh_token", "")
|
|
228
|
+
if refresh_tok:
|
|
229
|
+
result = _gotrue_post("/token?grant_type=refresh_token", {"refresh_token": refresh_tok})
|
|
230
|
+
if result and result.get("access_token"):
|
|
231
|
+
token["access_token"] = result["access_token"]
|
|
232
|
+
token["refresh_token"] = result.get("refresh_token", refresh_tok)
|
|
233
|
+
token["expires_at"] = time.strftime(
|
|
234
|
+
"%Y-%m-%dT%H:%M:%S+00:00",
|
|
235
|
+
time.gmtime(_now_ts() + TOKEN_TTL_SECONDS),
|
|
236
|
+
)
|
|
237
|
+
token["refreshed_at"] = _now_iso()
|
|
238
|
+
token["mode"] = "gotrue"
|
|
239
|
+
return token
|
|
240
|
+
|
|
241
|
+
# Offline: extend TTL locally
|
|
242
|
+
token["expires_at"] = time.strftime(
|
|
243
|
+
"%Y-%m-%dT%H:%M:%S+00:00",
|
|
244
|
+
time.gmtime(_now_ts() + TOKEN_TTL_SECONDS),
|
|
245
|
+
)
|
|
246
|
+
token["refreshed_at"] = _now_iso()
|
|
247
|
+
token["mode"] = "offline"
|
|
248
|
+
return token
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def cmd_login(code: str = "") -> None:
|
|
252
|
+
if is_authenticated():
|
|
253
|
+
token = load_token()
|
|
254
|
+
print(f"[0dai] already authenticated as {token.get('user', '?')}")
|
|
255
|
+
print(f"[0dai] plan: {token.get('plan', 'free')} ({token.get('team', '')})")
|
|
256
|
+
print("[0dai] run '0dai auth logout' first to re-authenticate")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
if not code:
|
|
260
|
+
print("[0dai] Authenticate with 0dai Cloud")
|
|
261
|
+
print("")
|
|
262
|
+
print(f" 1. Go to: {API_BASE}/auth")
|
|
263
|
+
print(" 2. Sign in or create an account")
|
|
264
|
+
print(" 3. Copy the auth code (format: XXXX-YYYY)")
|
|
265
|
+
print("")
|
|
266
|
+
print(" No account yet? Run: 0dai auth signup --email you@example.com")
|
|
267
|
+
print("")
|
|
268
|
+
try:
|
|
269
|
+
code = input("[0dai] Paste auth code: ").strip()
|
|
270
|
+
except (EOFError, KeyboardInterrupt):
|
|
271
|
+
print("\n[0dai] cancelled")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
if not code:
|
|
275
|
+
print("[0dai] no code provided")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
result = _validate_code(code)
|
|
279
|
+
if not result:
|
|
280
|
+
print("[0dai] invalid auth code. Format: XXXX-YYYY")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
save_token(result)
|
|
284
|
+
mode = f" ({result['mode']})" if result.get("mode") == "offline" else ""
|
|
285
|
+
print(f"[0dai] authenticated as {result['user']}{mode}")
|
|
286
|
+
print(f"[0dai] team: {result['team']}")
|
|
287
|
+
print(f"[0dai] plan: {result['plan']} ({result['seats']} seats)")
|
|
288
|
+
print(f"[0dai] token saved to {TOKEN_PATH}")
|
|
289
|
+
print(f"[0dai] expires: {result['expires_at'][:10]} (auto-refresh when online)")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def cmd_signup(email: str) -> None:
|
|
293
|
+
if not email or "@" not in email:
|
|
294
|
+
print("[0dai] invalid email")
|
|
295
|
+
sys.exit(1)
|
|
296
|
+
|
|
297
|
+
print(f"[0dai] creating account for {email}...")
|
|
298
|
+
result = _signup(email)
|
|
299
|
+
if not result:
|
|
300
|
+
print(f"[0dai] signup failed. Try again or visit {API_BASE}/auth")
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
|
|
303
|
+
mode = f" ({result['mode']})" if result.get("mode") == "offline" else ""
|
|
304
|
+
print(f"[0dai] account created{mode}")
|
|
305
|
+
print(f"[0dai] team: {result.get('team', '?')}")
|
|
306
|
+
|
|
307
|
+
# If GoTrue returned a token, auto-login
|
|
308
|
+
if result.get("access_token"):
|
|
309
|
+
token = {
|
|
310
|
+
"token_id": f"tok_{result.get('user_id', '')[:12]}",
|
|
311
|
+
"user": email,
|
|
312
|
+
"team": result.get("team", ""),
|
|
313
|
+
"plan": "free",
|
|
314
|
+
"seats": 1,
|
|
315
|
+
"authenticated_at": _now_iso(),
|
|
316
|
+
"expires_at": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime(_now_ts() + TOKEN_TTL_SECONDS)),
|
|
317
|
+
"access_token": result["access_token"],
|
|
318
|
+
"refresh_token": result.get("refresh_token", ""),
|
|
319
|
+
"mode": "gotrue",
|
|
320
|
+
}
|
|
321
|
+
save_token(token)
|
|
322
|
+
print(f"[0dai] auto-logged in as {email}")
|
|
323
|
+
print(f"[0dai] token saved to {TOKEN_PATH}")
|
|
324
|
+
else:
|
|
325
|
+
print(f"[0dai] auth code: {result.get('auth_code', '?')}")
|
|
326
|
+
print("")
|
|
327
|
+
print(f" Login now: 0dai auth login --code {result.get('auth_code', '?')}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def cmd_refresh() -> None:
|
|
331
|
+
token = load_token()
|
|
332
|
+
if not token:
|
|
333
|
+
print("[0dai] not authenticated. Run: 0dai auth login")
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
print("[0dai] refreshing token...")
|
|
337
|
+
new_token = _refresh_token(token)
|
|
338
|
+
if new_token:
|
|
339
|
+
save_token(new_token)
|
|
340
|
+
mode = f" ({new_token['mode']})" if new_token.get("mode") == "offline" else ""
|
|
341
|
+
print(f"[0dai] token refreshed{mode}")
|
|
342
|
+
print(f"[0dai] expires: {new_token.get('expires_at', '?')[:10]}")
|
|
343
|
+
else:
|
|
344
|
+
print("[0dai] refresh failed. Run: 0dai auth login")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cmd_status() -> None:
|
|
348
|
+
token = load_token()
|
|
349
|
+
if not token:
|
|
350
|
+
print("[0dai] not authenticated")
|
|
351
|
+
print(" Run: 0dai auth login")
|
|
352
|
+
print(" Core CLI works without auth. Team features require authentication.")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
valid = is_authenticated()
|
|
356
|
+
mode = f" ({token.get('mode', 'cloud')})" if token.get("mode") else ""
|
|
357
|
+
print("Auth Status:")
|
|
358
|
+
print(f" User: {token.get('user', '?')}")
|
|
359
|
+
print(f" Team: {token.get('team', '?')}")
|
|
360
|
+
print(f" Plan: {token.get('plan', 'free')}")
|
|
361
|
+
print(f" Seats: {token.get('seats', '?')}")
|
|
362
|
+
print(f" Status: {'active' if valid else 'EXPIRED'}{mode}")
|
|
363
|
+
print(f" Expires: {token.get('expires_at', '?')[:10]}")
|
|
364
|
+
print(f" Token: {TOKEN_PATH}")
|
|
365
|
+
if not valid:
|
|
366
|
+
print("\n Token expired. Run '0dai auth login' to re-authenticate.")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def cmd_logout() -> None:
|
|
370
|
+
if not TOKEN_PATH.is_file():
|
|
371
|
+
print("[0dai] not authenticated")
|
|
372
|
+
return
|
|
373
|
+
remove_token()
|
|
374
|
+
print("[0dai] logged out. Token removed.")
|
|
375
|
+
print("[0dai] core CLI continues to work. Team features require re-authentication.")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cmd_json() -> None:
|
|
379
|
+
token = load_token()
|
|
380
|
+
print(json.dumps({
|
|
381
|
+
"authenticated": is_authenticated(),
|
|
382
|
+
"plan": get_plan(),
|
|
383
|
+
"token": token,
|
|
384
|
+
}, indent=2, ensure_ascii=False))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def require_auth(feature_name: str = "This feature") -> None:
|
|
388
|
+
if is_authenticated() and check_cloud_tier("team"):
|
|
389
|
+
return
|
|
390
|
+
print(f"[0dai] {feature_name} requires a Team plan.")
|
|
391
|
+
print("")
|
|
392
|
+
if not is_authenticated():
|
|
393
|
+
print(" Authenticate: 0dai auth login")
|
|
394
|
+
print(" Create account: 0dai auth signup --email you@example.com")
|
|
395
|
+
print(f" Pricing: {API_BASE}/pricing")
|
|
396
|
+
else:
|
|
397
|
+
print(f" Current plan: {get_plan()}")
|
|
398
|
+
print(f" Upgrade: {API_BASE}/pricing")
|
|
399
|
+
print("")
|
|
400
|
+
print(" Free tier: init-existing, sync, doctor, detect, serve, harvest, promote, search, mcp.")
|
|
401
|
+
sys.exit(0)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def main() -> None:
|
|
405
|
+
subcmd = "status"
|
|
406
|
+
code = ""
|
|
407
|
+
email = ""
|
|
408
|
+
json_mode = False
|
|
409
|
+
args = sys.argv[1:]
|
|
410
|
+
|
|
411
|
+
i = 0
|
|
412
|
+
while i < len(args):
|
|
413
|
+
if args[i] == "--code" and i + 1 < len(args):
|
|
414
|
+
code = args[i + 1]
|
|
415
|
+
i += 2
|
|
416
|
+
elif args[i] == "--email" and i + 1 < len(args):
|
|
417
|
+
email = args[i + 1]
|
|
418
|
+
i += 2
|
|
419
|
+
elif args[i] == "--json":
|
|
420
|
+
json_mode = True
|
|
421
|
+
i += 1
|
|
422
|
+
elif args[i] in ("login", "status", "logout", "signup", "refresh"):
|
|
423
|
+
subcmd = args[i]
|
|
424
|
+
i += 1
|
|
425
|
+
else:
|
|
426
|
+
i += 1
|
|
427
|
+
|
|
428
|
+
if json_mode:
|
|
429
|
+
cmd_json()
|
|
430
|
+
elif subcmd == "login":
|
|
431
|
+
cmd_login(code)
|
|
432
|
+
elif subcmd == "signup":
|
|
433
|
+
cmd_signup(email)
|
|
434
|
+
elif subcmd == "refresh":
|
|
435
|
+
cmd_refresh()
|
|
436
|
+
elif subcmd == "logout":
|
|
437
|
+
cmd_logout()
|
|
438
|
+
else:
|
|
439
|
+
cmd_status()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if __name__ == "__main__":
|
|
443
|
+
main()
|