akashcli 4.7.0__tar.gz → 4.7.2__tar.gz
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.
- {akashcli-4.7.0 → akashcli-4.7.2}/PKG-INFO +1 -1
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/__init__.py +1 -1
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/cli.py +106 -228
- akashcli-4.7.2/akashcli/updater.py +225 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli.egg-info/SOURCES.txt +1 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/pyproject.toml +1 -1
- {akashcli-4.7.0 → akashcli-4.7.2}/README.md +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/_net.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/auth.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/client.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/config.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/crypto.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/env_manager.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/exceptions.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/exec.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/integrity.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/logger.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/models.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/p2p.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/packages.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/relay.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/restore.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/storage.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/terminal.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/uploader.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli/utils.py +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.7.0 → akashcli-4.7.2}/setup.cfg +0 -0
|
@@ -6,6 +6,7 @@ import subprocess
|
|
|
6
6
|
import re
|
|
7
7
|
import webbrowser
|
|
8
8
|
import tempfile
|
|
9
|
+
import atexit
|
|
9
10
|
from typing import Optional, List
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
@@ -18,6 +19,9 @@ from .config import ConfigManager, DEFAULT_STORAGE_URL
|
|
|
18
19
|
from .exceptions import AkashError
|
|
19
20
|
from .exec import _device_fingerprint
|
|
20
21
|
from .uploader import VaultUploader
|
|
22
|
+
from .updater import show_update_warning_if_available, check_for_updates, format_update_box, mark_update_printed
|
|
23
|
+
|
|
24
|
+
atexit.register(show_update_warning_if_available)
|
|
21
25
|
|
|
22
26
|
# ── ANSI palette (matches exec.py) ───────────────────────────────────────────
|
|
23
27
|
_C = "\033[1;36m" # cyan bold
|
|
@@ -184,6 +188,13 @@ def _version_callback(value: bool):
|
|
|
184
188
|
if value:
|
|
185
189
|
from . import __version__
|
|
186
190
|
print(f"akashcli v{__version__}")
|
|
191
|
+
try:
|
|
192
|
+
available, latest = check_for_updates(__version__, force_sync=False)
|
|
193
|
+
if available and latest:
|
|
194
|
+
print(format_update_box(__version__, latest))
|
|
195
|
+
mark_update_printed()
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
187
198
|
raise typer.Exit(0)
|
|
188
199
|
|
|
189
200
|
def raise_exit():
|
|
@@ -204,40 +215,46 @@ def _print_help():
|
|
|
204
215
|
C, G, Y, DM, RS = _C, _G, _Y, _DM, _RS
|
|
205
216
|
lines = [
|
|
206
217
|
"",
|
|
207
|
-
f" {C}akash{RS} {DM}
|
|
218
|
+
f" {C}akash{RS} {DM}— encrypted cloud backup · remote shell · AI agent vault{RS}",
|
|
208
219
|
"",
|
|
209
|
-
f" {DM}
|
|
210
|
-
f" {C}
|
|
211
|
-
f" {C}
|
|
220
|
+
f" {DM}ACCOUNT & SERVER{RS}",
|
|
221
|
+
f" {C}login{RS} {DM}[--label NAME] [--url URL]{RS} save your API key + name this device",
|
|
222
|
+
f" {C}whoami{RS} account quota, storage URL & device info",
|
|
223
|
+
f" {C}url{RS} {DM}[NEW_URL|--reset]{RS} view or switch configured storage server URL",
|
|
224
|
+
f" {C}logout{RS} clear saved credentials from this device",
|
|
212
225
|
"",
|
|
213
226
|
f" {DM}ENVIRONMENT BACKUP{RS}",
|
|
214
|
-
f" {C}backup{RS} {DM}[--password
|
|
215
|
-
f" {C}restore{RS} {DM}<CODE|PATH>{RS}
|
|
216
|
-
f" {C}
|
|
217
|
-
f" {C}
|
|
227
|
+
f" {C}backup{RS} {DM}[--label NAME] [--password PASS]{RS} save environment & packages to cloud or disk",
|
|
228
|
+
f" {C}restore{RS} {DM}<CODE|PATH> [--dry-run]{RS} restore terminal environment from cloud or archive",
|
|
229
|
+
f" {C}saves{RS} {DM}[--json]{RS} list cloud-saved and local environments",
|
|
230
|
+
f" {C}pull{RS} {DM}<CODE> [--target DIR]{RS} one-shot restore on fresh machine (no login needed)",
|
|
231
|
+
"",
|
|
232
|
+
f" {DM}REMOTE TERMINAL & SHELL{RS}",
|
|
233
|
+
f" {C}shell{RS} {DM}[DEVICE|--code CODE]{RS} open interactive shell (local, device, or code)",
|
|
234
|
+
f" {C}run{RS} {DM}<CMD> [--device NAME]{RS} run command on server or enrolled device",
|
|
235
|
+
f" {C}serve{RS} {DM}[--share] [--label NAME]{RS} make this device reachable (or get one-time code)",
|
|
236
|
+
f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or manage enrolled devices",
|
|
218
237
|
"",
|
|
219
|
-
f" {DM}
|
|
220
|
-
f" {C}
|
|
221
|
-
f" {C}run{RS} {DM}<CMD> [--device NAME]{RS} run command on server or enrolled device",
|
|
222
|
-
f" {C}serve{RS} {DM}[--share]{RS} make this device reachable (or get one-time code)",
|
|
223
|
-
f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or manage enrolled devices",
|
|
238
|
+
f" {DM}AI AGENTS & MCP{RS}",
|
|
239
|
+
f" {C}mcp{RS} {DM}<serve|share|create|list…>{RS} AI agent & MCP tool bridge (Codex, Claude, Cursor)",
|
|
224
240
|
"",
|
|
225
|
-
f" {DM}FILES &
|
|
226
|
-
f" {C}cloud{RS} {DM}<upload|files|
|
|
227
|
-
f" {C}
|
|
228
|
-
f" {C}inbox{RS} {DM}[CODE]{RS} receive & decrypt files sent to your identity",
|
|
229
|
-
f" {C}id{RS} {DM}[--renew]{RS} view or renew your P2P identity fingerprint",
|
|
241
|
+
f" {DM}FILES & CLOUD VAULT{RS}",
|
|
242
|
+
f" {C}cloud{RS} {DM}<upload|files|stream|delete…>{RS} cloud file vault & media streaming engine",
|
|
243
|
+
f" {C}pipe{RS} {DM}[--name NAME] [--burn]{RS} pipe stdin or command output directly to cloud vault",
|
|
230
244
|
"",
|
|
231
|
-
f" {DM}
|
|
232
|
-
f" {C}
|
|
233
|
-
f" {C}
|
|
234
|
-
f" {C}
|
|
235
|
-
f" {C}url{RS} {DM}[NEW_URL|--reset]{RS} view or switch configured storage server URL",
|
|
236
|
-
f" {C}version{RS} check CLI and server status & versions",
|
|
237
|
-
f" {C}env{RS} {DM}<set|get|list…>{RS} encrypted local .env store",
|
|
238
|
-
f" {C}check{RS} {DM}[--seal]{RS} verify source integrity & tamper detection",
|
|
245
|
+
f" {DM}SECURE P2P TRANSFERS{RS}",
|
|
246
|
+
f" {C}send{RS} {DM}<USER> <FILE>{RS} end-to-end encrypted P2P file transfer",
|
|
247
|
+
f" {C}inbox{RS} {DM}[CODE]{RS} receive & decrypt files sent to your identity",
|
|
248
|
+
f" {C}id{RS} {DM}[--export|--add]{RS} show P2P fingerprint, export key, or add contacts",
|
|
239
249
|
"",
|
|
240
|
-
f" {DM}
|
|
250
|
+
f" {DM}SECRETS & SYSTEM{RS}",
|
|
251
|
+
f" {C}env{RS} {DM}<set|get|list|export|push…>{RS} encrypted local .env store & cloud sync",
|
|
252
|
+
f" {C}log{RS} {DM}[--stats]{RS} view operation history & bandwidth summary",
|
|
253
|
+
f" {C}check{RS} {DM}[--seal]{RS} verify source integrity & tamper detection",
|
|
254
|
+
f" {C}version{RS} check CLI and server status & versions",
|
|
255
|
+
f" {C}update{RS} check and install latest akashcli release",
|
|
256
|
+
"",
|
|
257
|
+
f" {DM}Run {RS}{C}akash <command> --help{RS}{DM} for full options on any command.{RS}",
|
|
241
258
|
"",
|
|
242
259
|
]
|
|
243
260
|
nl = chr(10)
|
|
@@ -312,7 +329,50 @@ def version(url: Optional[str] = typer.Option(None, "--url", help="Server URL to
|
|
|
312
329
|
print(f" {_DM}server {_RS}{_cy(base)} {_dim(f'(HTTP {resp.status_code})')}")
|
|
313
330
|
except Exception:
|
|
314
331
|
print(f" {_DM}server {_RS}{_cy(base)} {_dim('(unreachable)')}")
|
|
315
|
-
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
available, latest = check_for_updates(__version__, force_sync=True)
|
|
335
|
+
if available and latest:
|
|
336
|
+
print(format_update_box(__version__, latest))
|
|
337
|
+
mark_update_printed()
|
|
338
|
+
else:
|
|
339
|
+
print(f" {_G}✓{_RS} {_DM}akashcli is up to date{_RS}\n")
|
|
340
|
+
except Exception:
|
|
341
|
+
print()
|
|
342
|
+
|
|
343
|
+
@app.command()
|
|
344
|
+
def update():
|
|
345
|
+
"""Check for updates and upgrade akashcli to the latest version."""
|
|
346
|
+
from . import __version__
|
|
347
|
+
_p()
|
|
348
|
+
print(f" {_C}◈{_RS} {_W}Checking for akashcli updates…{_RS}")
|
|
349
|
+
available, latest = check_for_updates(__version__, force_sync=True)
|
|
350
|
+
if not available or not latest:
|
|
351
|
+
_ok(f"akashcli is already at the latest version ({_cy('v' + __version__)})")
|
|
352
|
+
_p()
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
print(f" {_Y}⚡{_RS} New version available: {_DM}{__version__}{_RS} {_C}→{_RS} {_G}{latest}{_RS}")
|
|
356
|
+
with _Spin(f"updating akashcli to v{latest}…"):
|
|
357
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", "akashcli", "--break-system-packages"]
|
|
358
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
359
|
+
if res.returncode != 0:
|
|
360
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", "akashcli"]
|
|
361
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
362
|
+
|
|
363
|
+
if res.returncode == 0:
|
|
364
|
+
_ok(f"Successfully updated akashcli to {_G}v{latest}{_RS}!")
|
|
365
|
+
try:
|
|
366
|
+
from .integrity import seal
|
|
367
|
+
seal()
|
|
368
|
+
_ok("Integrity manifest re-sealed.")
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
else:
|
|
372
|
+
_err(f"Update failed: {res.stderr or res.stdout}")
|
|
373
|
+
_info(f"Please run manually: {_cy('pip install -U akashcli')}")
|
|
374
|
+
mark_update_printed()
|
|
375
|
+
_p()
|
|
316
376
|
|
|
317
377
|
@app.command()
|
|
318
378
|
def whoami():
|
|
@@ -2670,8 +2730,10 @@ def env_pull(
|
|
|
2670
2730
|
# AKASH AGENT & DEVICE BINDING COMMANDS
|
|
2671
2731
|
# =============================================================================
|
|
2672
2732
|
|
|
2673
|
-
|
|
2674
|
-
|
|
2733
|
+
mcp_app = typer.Typer(help="Model Context Protocol (MCP) server & AI agent tool bridge.")
|
|
2734
|
+
agent_app = mcp_app
|
|
2735
|
+
app.add_typer(mcp_app, name="mcp", help="MCP server · AI agent tools · Claude/Cursor/Codex bridge")
|
|
2736
|
+
app.add_typer(mcp_app, name="agent", hidden=True, help="Alias for akash mcp")
|
|
2675
2737
|
|
|
2676
2738
|
def _render_inspection_card(r: dict) -> None:
|
|
2677
2739
|
print(f"\n {_C}╭─ ⚡ INCOMING AGENT DEVICE REGISTRATION ────────────────────────────{_RS}")
|
|
@@ -2861,6 +2923,8 @@ def agent_update(
|
|
|
2861
2923
|
_err("Provide a Profile ID (e.g. 'akash agent update fbf6 --scopes +exec:package_install')")
|
|
2862
2924
|
raise typer.Exit(1)
|
|
2863
2925
|
|
|
2926
|
+
if role and not role.startswith("@") and role in ("viewer", "coder", "tester", "full"):
|
|
2927
|
+
role = "@" + role
|
|
2864
2928
|
effective_scopes = scopes if scopes else role
|
|
2865
2929
|
if not effective_scopes:
|
|
2866
2930
|
_err("Specify new scopes with --scopes or --role (e.g. --scopes +exec:package_install or --role @coder)")
|
|
@@ -2932,6 +2996,13 @@ def agent_create(
|
|
|
2932
2996
|
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
2933
2997
|
):
|
|
2934
2998
|
"""Generate new Client ID and Client Secret with hardware binding."""
|
|
2999
|
+
if role:
|
|
3000
|
+
if not role.startswith("@") and role in ("viewer", "coder", "tester", "full"):
|
|
3001
|
+
role = "@" + role
|
|
3002
|
+
effective_scopes = role
|
|
3003
|
+
else:
|
|
3004
|
+
effective_scopes = scopes
|
|
3005
|
+
|
|
2935
3006
|
api_key, base = _resolve_client(url, require_key=False)
|
|
2936
3007
|
hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
|
|
2937
3008
|
if api_key: hdrs["x-api-key"] = api_key
|
|
@@ -2983,30 +3054,6 @@ def agent_create(
|
|
|
2983
3054
|
print(f" {_G}akash mcp serve --id {profile_id}{_RS}")
|
|
2984
3055
|
_p()
|
|
2985
3056
|
|
|
2986
|
-
@agent_app.command("update")
|
|
2987
|
-
def agent_update(
|
|
2988
|
-
client_id: str = typer.Argument(..., help="Client ID of the agent to update"),
|
|
2989
|
-
scopes: str = typer.Option(..., "--scopes", "-s", help="New scopes or role preset (e.g., @coder, +exec:package_install)"),
|
|
2990
|
-
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
2991
|
-
):
|
|
2992
|
-
"""Update permissions/scopes for an existing AI Agent application."""
|
|
2993
|
-
api_key, base = _resolve_client(url, require_key=False)
|
|
2994
|
-
hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
|
|
2995
|
-
if api_key: hdrs["x-api-key"] = api_key
|
|
2996
|
-
|
|
2997
|
-
with _Spin(f"updating permissions for {client_id}…"):
|
|
2998
|
-
res = httpx.patch(
|
|
2999
|
-
f"{base}/api/agent/app/{client_id}",
|
|
3000
|
-
headers=hdrs,
|
|
3001
|
-
json={"scopes": scopes},
|
|
3002
|
-
timeout=10.0
|
|
3003
|
-
)
|
|
3004
|
-
if res.status_code == 200:
|
|
3005
|
-
d = res.json()
|
|
3006
|
-
_ok(f"Agent {_ye(client_id)} permissions updated to: {_cy(d.get('scopes', ''))}")
|
|
3007
|
-
else:
|
|
3008
|
-
_err(f"Failed to update scopes: {res.text}")
|
|
3009
|
-
|
|
3010
3057
|
@agent_app.command("requests")
|
|
3011
3058
|
def agent_requests(url: Optional[str] = typer.Option(None, "--url", help="Override server URL")):
|
|
3012
3059
|
"""List pending device registration requests."""
|
|
@@ -3094,64 +3141,11 @@ def agent_reject(request_id: str, url: Optional[str] = typer.Option(None, "--url
|
|
|
3094
3141
|
else:
|
|
3095
3142
|
_err(f"Rejection failed: {res.text}")
|
|
3096
3143
|
|
|
3097
|
-
@app.command("guard")
|
|
3098
|
-
def guard_console(url: Optional[str] = typer.Option(None, "--url", help="Override server URL")):
|
|
3099
|
-
import time
|
|
3100
|
-
"""Interactive real-time terminal console that listens for agent access requests."""
|
|
3101
|
-
api_key, base = _resolve_client(url, require_key=False)
|
|
3102
|
-
hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
|
|
3103
|
-
if api_key: hdrs["x-api-key"] = api_key
|
|
3104
|
-
print(f"\n {_G}◉{_RS} {_W}Akash Guard Console Online{_RS}")
|
|
3105
|
-
print(f" {_DM}Listening for incoming agent device registrations (Ctrl-C to stop)…{_RS}\n")
|
|
3106
|
-
|
|
3107
|
-
handled_requests = set()
|
|
3108
|
-
while True:
|
|
3109
|
-
try:
|
|
3110
|
-
res = httpx.get(
|
|
3111
|
-
f"{base}/api/agent/requests",
|
|
3112
|
-
headers=hdrs,
|
|
3113
|
-
timeout=5.0
|
|
3114
|
-
)
|
|
3115
|
-
if res.status_code == 200:
|
|
3116
|
-
for r in res.json().get("requests", []):
|
|
3117
|
-
rid = r["request_id"]
|
|
3118
|
-
if r["status"] == "pending" and rid not in handled_requests:
|
|
3119
|
-
_render_inspection_card(r)
|
|
3120
|
-
try:
|
|
3121
|
-
sys.stdout.write(f" Approve access for this device? [y/N]: ")
|
|
3122
|
-
sys.stdout.flush()
|
|
3123
|
-
choice = sys.stdin.readline().strip().lower()
|
|
3124
|
-
except (KeyboardInterrupt, EOFError):
|
|
3125
|
-
choice = "n"
|
|
3126
|
-
|
|
3127
|
-
if choice == "y":
|
|
3128
|
-
httpx.post(f"{base}/api/agent/requests/{rid}/approve", headers=hdrs, timeout=5)
|
|
3129
|
-
_ok(f"Approved {rid} - Access token released to agent!\n")
|
|
3130
|
-
else:
|
|
3131
|
-
httpx.post(f"{base}/api/agent/requests/{rid}/reject", headers=hdrs, timeout=5)
|
|
3132
|
-
_warn(f"Rejected {rid}.\n")
|
|
3133
|
-
handled_requests.add(rid)
|
|
3134
|
-
time.sleep(2.0)
|
|
3135
|
-
except KeyboardInterrupt:
|
|
3136
|
-
print(f"\n {_DM}Guard console stopped.{_RS}\n")
|
|
3137
|
-
break
|
|
3138
|
-
except Exception:
|
|
3139
|
-
time.sleep(3.0)
|
|
3140
|
-
|
|
3141
3144
|
|
|
3142
3145
|
# =============================================================================
|
|
3143
|
-
# MODEL CONTEXT PROTOCOL (MCP)
|
|
3146
|
+
# MODEL CONTEXT PROTOCOL (MCP) & AI AGENT ENGINE (MERGED)
|
|
3144
3147
|
# =============================================================================
|
|
3145
3148
|
|
|
3146
|
-
mcp_app = typer.Typer(help="Model Context Protocol (MCP) server for AI agents.")
|
|
3147
|
-
app.add_typer(mcp_app, name="mcp", help="Model Context Protocol (MCP) bridge for AI agents")
|
|
3148
|
-
mcp_app.command("guard", help="Terminal approval console for incoming agent requests")(guard_console)
|
|
3149
|
-
|
|
3150
|
-
@mcp_app.command("list")
|
|
3151
|
-
def mcp_list(raw: bool = typer.Option(False, "--json", help="Raw JSON output")):
|
|
3152
|
-
"""List all available Agent Profile IDs for MCP."""
|
|
3153
|
-
agent_list(raw=raw)
|
|
3154
|
-
|
|
3155
3149
|
_ASYNC_JOBS = {}
|
|
3156
3150
|
_LAST_UNDO_DIR = Path.home() / ".akash" / "undo_vault"
|
|
3157
3151
|
|
|
@@ -3268,7 +3262,7 @@ def _parse_temp_duration(val: str) -> int:
|
|
|
3268
3262
|
elif unit in ('d', 'day', 'days'): return num * 1440
|
|
3269
3263
|
return num
|
|
3270
3264
|
|
|
3271
|
-
@
|
|
3265
|
+
@agent_app.command("serve")
|
|
3272
3266
|
def mcp_serve(
|
|
3273
3267
|
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
3274
3268
|
client_id: Optional[str] = typer.Option(None, "--client-id", help="Agent Client ID"),
|
|
@@ -3449,8 +3443,8 @@ def mcp_serve(
|
|
|
3449
3443
|
print(f" {_GR}{now_t}{_RS} {_Y}⚠{_RS} {_dim(f'Worker disconnected ({e}). Reconnecting in {backoff:.1f}s (attempt {reconnect_attempts})…')}")
|
|
3450
3444
|
time.sleep(backoff)
|
|
3451
3445
|
|
|
3452
|
-
@
|
|
3453
|
-
@
|
|
3446
|
+
@agent_app.command("share")
|
|
3447
|
+
@agent_app.command("url", hidden=True)
|
|
3454
3448
|
def mcp_share(
|
|
3455
3449
|
target: Optional[str] = typer.Argument(None, help="Profile ID (optional if only 1 profile exists)"),
|
|
3456
3450
|
id: Optional[str] = typer.Option(None, "--id", "-i", help="Profile ID"),
|
|
@@ -3642,123 +3636,7 @@ def mcp_share(
|
|
|
3642
3636
|
_p()
|
|
3643
3637
|
|
|
3644
3638
|
|
|
3645
|
-
@
|
|
3646
|
-
def mcp_share(
|
|
3647
|
-
target: Optional[str] = typer.Argument(None, help="Profile ID (optional if only 1 profile exists)"),
|
|
3648
|
-
id: Optional[str] = typer.Option(None, "--id", "-i", help="Profile ID"),
|
|
3649
|
-
temp: Optional[str] = typer.Option(None, "--temp", "-t", help="Temporary short URL duration (e.g. '30m', '30 min', '1h')"),
|
|
3650
|
-
codex: bool = typer.Option(False, "--codex", "-c", help="Show snippet only for Codex CLI/Desktop"),
|
|
3651
|
-
claude: bool = typer.Option(False, "--claude", help="Show snippet only for Claude Desktop / Cursor"),
|
|
3652
|
-
keys: bool = typer.Option(False, "--keys", "-k", help="Show raw Client ID and Secret"),
|
|
3653
|
-
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
3654
|
-
):
|
|
3655
|
-
"""Generate ready-to-copy MCP configuration for Codex, Claude, or Cursor on ANY device."""
|
|
3656
|
-
import json, urllib.parse
|
|
3657
|
-
from pathlib import Path
|
|
3658
|
-
|
|
3659
|
-
agents_file = Path.home() / ".akash" / "agents.json"
|
|
3660
|
-
if not agents_file.exists():
|
|
3661
|
-
_err("No saved agent profiles found. Create one with: akash agent create")
|
|
3662
|
-
raise typer.Exit(1)
|
|
3663
|
-
|
|
3664
|
-
try:
|
|
3665
|
-
agents = json.loads(agents_file.read_text(encoding="utf-8"))
|
|
3666
|
-
except Exception: agents = {}
|
|
3667
|
-
|
|
3668
|
-
target_id = id or target
|
|
3669
|
-
if not target_id:
|
|
3670
|
-
if len(agents) == 1:
|
|
3671
|
-
target_id = next(iter(agents.keys()))
|
|
3672
|
-
elif "af4f" in agents:
|
|
3673
|
-
target_id = "af4f"
|
|
3674
|
-
else:
|
|
3675
|
-
_err("Multiple profiles found. Specify which one: akash mcp share <ID>")
|
|
3676
|
-
raise typer.Exit(1)
|
|
3677
|
-
|
|
3678
|
-
pid, cid, name = _resolve_agent_id(target_id)
|
|
3679
|
-
profile = agents.get(pid, {})
|
|
3680
|
-
csec = profile.get("client_secret", "")
|
|
3681
|
-
|
|
3682
|
-
if not csec:
|
|
3683
|
-
_err(f"No client_secret found for profile {pid}.")
|
|
3684
|
-
raise typer.Exit(1)
|
|
3685
|
-
|
|
3686
|
-
_, base = _resolve_client(url or profile.get("url"), require_key=False, silent=True)
|
|
3687
|
-
dev_fp = _device_fingerprint()
|
|
3688
|
-
|
|
3689
|
-
lan_ip = _get_lan_ip()
|
|
3690
|
-
port = base.split(":")[-1] if ":" in base.replace("http://", "").replace("https://", "") else "8000"
|
|
3691
|
-
wifi_base = f"http://{lan_ip}:{port}"
|
|
3692
|
-
|
|
3693
|
-
if temp:
|
|
3694
|
-
try:
|
|
3695
|
-
ttl_min = _parse_temp_duration(temp)
|
|
3696
|
-
except ValueError as ve:
|
|
3697
|
-
_err(str(ve))
|
|
3698
|
-
raise typer.Exit(1)
|
|
3699
|
-
|
|
3700
|
-
with _Spin(f"generating {ttl_min}m short link…"):
|
|
3701
|
-
try:
|
|
3702
|
-
res = httpx.post(
|
|
3703
|
-
f"{base}/api/agent/mcp/ticket",
|
|
3704
|
-
json={"client_id": cid, "client_secret": csec, "device_fp": dev_fp, "ttl_minutes": ttl_min},
|
|
3705
|
-
timeout=10.0
|
|
3706
|
-
)
|
|
3707
|
-
res.raise_for_status()
|
|
3708
|
-
d = res.json()
|
|
3709
|
-
tcode = d["ticket_code"]
|
|
3710
|
-
local_url = f"{base}/mcp/t/{tcode}"
|
|
3711
|
-
wifi_url = f"{wifi_base}/mcp/t/{tcode}"
|
|
3712
|
-
mode_label = f"Temporary ({ttl_min} mins · Ephemeral)"
|
|
3713
|
-
except Exception as e:
|
|
3714
|
-
_err(f"Failed to issue temporary ticket: {e}")
|
|
3715
|
-
raise typer.Exit(1)
|
|
3716
|
-
else:
|
|
3717
|
-
params = urllib.parse.urlencode({"cid": cid, "csec": csec, "dfp": dev_fp})
|
|
3718
|
-
local_url = f"{base}/mcp?{params}"
|
|
3719
|
-
wifi_url = f"{wifi_base}/mcp?{params}"
|
|
3720
|
-
mode_label = "Permanent Small & Secure"
|
|
3721
|
-
|
|
3722
|
-
_p()
|
|
3723
|
-
print(" " + _C + "◈" + _RS + " " + _W + "Akash MCP · Quick Connect" + _RS)
|
|
3724
|
-
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
3725
|
-
print(" " + _DM + "Agent : " + _RS + _ye(name) + " " + _dim("(" + pid + ")"))
|
|
3726
|
-
print(" " + _DM + "Mode : " + _RS + _C + mode_label + _RS)
|
|
3727
|
-
if keys:
|
|
3728
|
-
print(" " + _DM + "Client ID: " + _RS + _dim(cid))
|
|
3729
|
-
print(" " + _DM + "Secret : " + _RS + _dim(csec))
|
|
3730
|
-
print(" " + _DM + "Device FP: " + _RS + _dim(dev_fp))
|
|
3731
|
-
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
3732
|
-
_p()
|
|
3733
|
-
|
|
3734
|
-
show_all = not codex and not claude
|
|
3735
|
-
|
|
3736
|
-
if show_all or codex:
|
|
3737
|
-
print(" " + _C + "For Codex CLI / Desktop" + _RS + " " + _dim("(~/.codex/config.toml)") + ":")
|
|
3738
|
-
print(" " + _DM + "Option A: Same Device (Termux / Localhost)" + _RS)
|
|
3739
|
-
print(" [mcp_servers.akash]")
|
|
3740
|
-
print(' url = "' + local_url + '"')
|
|
3741
|
-
if lan_ip != "127.0.0.1":
|
|
3742
|
-
print()
|
|
3743
|
-
print(" " + _DM + "Option B: Other Device (Over Wi-Fi)" + _RS)
|
|
3744
|
-
print(" [mcp_servers.akash]")
|
|
3745
|
-
print(' url = "' + wifi_url + '"')
|
|
3746
|
-
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
3747
|
-
_p()
|
|
3748
|
-
|
|
3749
|
-
if show_all or claude:
|
|
3750
|
-
print(" " + _C + "For Claude Desktop / Cursor" + _RS + " " + _dim("(config.json)") + ":")
|
|
3751
|
-
claude_cfg = json.dumps({"mcpServers": {"akash": {"url": local_url}}}, indent=2)
|
|
3752
|
-
for c_line in claude_cfg.splitlines():
|
|
3753
|
-
print(" " + c_line)
|
|
3754
|
-
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
3755
|
-
_p()
|
|
3756
|
-
|
|
3757
|
-
_info("Paste snippet into AI client, then keep running:")
|
|
3758
|
-
_info(f" {_cy('akash mcp serve')}")
|
|
3759
|
-
_p()
|
|
3760
|
-
|
|
3761
|
-
@mcp_app.command("status")
|
|
3639
|
+
@agent_app.command("status")
|
|
3762
3640
|
def mcp_status(
|
|
3763
3641
|
url: Optional[str] = typer.Option(None, "--url", help="Override server URL")
|
|
3764
3642
|
):
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
updater.py — Asynchronous, cached PyPI update checker for akashcli.
|
|
3
|
+
|
|
4
|
+
- Queries PyPI (https://pypi.org/pypi/akashcli/json) for the latest release.
|
|
5
|
+
- Caches response in ~/.akash/update_check.json with a 6-hour TTL.
|
|
6
|
+
- Background refresh runs detached so CLI commands have 0ms network latency.
|
|
7
|
+
- Renders an aligned, non-intrusive warning box when an update is available.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import subprocess
|
|
15
|
+
import unicodedata
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, Dict, Tuple
|
|
19
|
+
|
|
20
|
+
CACHE_FILE = Path.home() / ".akash" / "update_check.json"
|
|
21
|
+
CHECK_INTERVAL_SECONDS = 6 * 3600 # 6 hours
|
|
22
|
+
PYPI_URL = "https://pypi.org/pypi/akashcli/json"
|
|
23
|
+
|
|
24
|
+
_already_printed = False
|
|
25
|
+
|
|
26
|
+
# ANSI Color Palette
|
|
27
|
+
_C = "\033[1;36m" # cyan bold
|
|
28
|
+
_W = "\033[1;37m" # white bold
|
|
29
|
+
_G = "\033[1;32m" # green bold
|
|
30
|
+
_Y = "\033[1;33m" # yellow bold
|
|
31
|
+
_DM = "\033[2m" # dim
|
|
32
|
+
_GR = "\033[90m" # gray
|
|
33
|
+
_RS = "\033[0m" # reset
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cell_width(text: str) -> int:
|
|
37
|
+
"""Calculate terminal display width accounting for wide characters and ANSI codes."""
|
|
38
|
+
clean = re.sub(r'\x1b\[[0-9;]*m', '', text)
|
|
39
|
+
width = 0
|
|
40
|
+
for ch in clean:
|
|
41
|
+
if unicodedata.east_asian_width(ch) in ('W', 'F'):
|
|
42
|
+
width += 2
|
|
43
|
+
else:
|
|
44
|
+
width += 1
|
|
45
|
+
return width
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_newer_version(latest: str, current: str) -> bool:
|
|
49
|
+
"""Compare two semantic version strings safely."""
|
|
50
|
+
if not latest or not current:
|
|
51
|
+
return False
|
|
52
|
+
try:
|
|
53
|
+
from packaging.version import parse
|
|
54
|
+
return parse(latest) > parse(current)
|
|
55
|
+
except Exception:
|
|
56
|
+
try:
|
|
57
|
+
def to_tuple(v: str) -> Tuple[int, ...]:
|
|
58
|
+
nums = [int(x) for x in re.findall(r'\d+', v.split("+")[0])]
|
|
59
|
+
return tuple(nums)
|
|
60
|
+
return to_tuple(latest) > to_tuple(current)
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fetch_latest_version(timeout: float = 2.0) -> Optional[str]:
|
|
66
|
+
"""Synchronously fetch the latest version of akashcli from PyPI."""
|
|
67
|
+
try:
|
|
68
|
+
import httpx
|
|
69
|
+
with httpx.Client(timeout=timeout) as client:
|
|
70
|
+
resp = client.get(PYPI_URL)
|
|
71
|
+
if resp.status_code == 200:
|
|
72
|
+
data = resp.json()
|
|
73
|
+
return data.get("info", {}).get("version")
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_cache() -> Optional[Dict]:
|
|
80
|
+
"""Read the cached update check result."""
|
|
81
|
+
try:
|
|
82
|
+
if CACHE_FILE.exists():
|
|
83
|
+
return json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def write_cache(latest_version: str):
|
|
90
|
+
"""Write the latest version to cache."""
|
|
91
|
+
try:
|
|
92
|
+
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
data = {
|
|
94
|
+
"last_checked": time.time(),
|
|
95
|
+
"latest_version": latest_version
|
|
96
|
+
}
|
|
97
|
+
CACHE_FILE.write_text(json.dumps(data), encoding="utf-8")
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def trigger_background_check():
|
|
103
|
+
"""Trigger a detached background process to query PyPI and update the cache."""
|
|
104
|
+
try:
|
|
105
|
+
subprocess.Popen(
|
|
106
|
+
[sys.executable, "-m", "akashcli.updater"],
|
|
107
|
+
stdout=subprocess.DEVNULL,
|
|
108
|
+
stderr=subprocess.DEVNULL,
|
|
109
|
+
stdin=subprocess.DEVNULL,
|
|
110
|
+
start_new_session=True,
|
|
111
|
+
close_fds=True,
|
|
112
|
+
)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_for_updates(current_version: str, force_sync: bool = False) -> Tuple[bool, Optional[str]]:
|
|
118
|
+
"""
|
|
119
|
+
Check if an update is available.
|
|
120
|
+
Returns (is_available, latest_version).
|
|
121
|
+
"""
|
|
122
|
+
if os.environ.get("AKASH_NO_UPDATE_CHECK", "").lower() in ("1", "true", "yes"):
|
|
123
|
+
return False, None
|
|
124
|
+
if os.environ.get("CI", "").lower() in ("1", "true", "yes"):
|
|
125
|
+
return False, None
|
|
126
|
+
|
|
127
|
+
now = time.time()
|
|
128
|
+
cache = read_cache()
|
|
129
|
+
|
|
130
|
+
if force_sync:
|
|
131
|
+
latest = fetch_latest_version(timeout=2.0)
|
|
132
|
+
if latest:
|
|
133
|
+
write_cache(latest)
|
|
134
|
+
return is_newer_version(latest, current_version), latest
|
|
135
|
+
# If fetch fails, fall back to cache
|
|
136
|
+
if cache and "latest_version" in cache:
|
|
137
|
+
latest = cache["latest_version"]
|
|
138
|
+
return is_newer_version(latest, current_version), latest
|
|
139
|
+
return False, None
|
|
140
|
+
|
|
141
|
+
# Check cache TTL
|
|
142
|
+
needs_refresh = True
|
|
143
|
+
if cache and isinstance(cache, dict):
|
|
144
|
+
last_checked = cache.get("last_checked", 0)
|
|
145
|
+
if (now - last_checked) < CHECK_INTERVAL_SECONDS:
|
|
146
|
+
needs_refresh = False
|
|
147
|
+
|
|
148
|
+
if needs_refresh:
|
|
149
|
+
trigger_background_check()
|
|
150
|
+
|
|
151
|
+
if cache and "latest_version" in cache:
|
|
152
|
+
latest = cache["latest_version"]
|
|
153
|
+
if is_newer_version(latest, current_version):
|
|
154
|
+
return True, latest
|
|
155
|
+
|
|
156
|
+
return False, None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def format_update_box(current: str, latest: str) -> str:
|
|
160
|
+
"""Format a styled, pixel-aligned terminal banner for the update notification."""
|
|
161
|
+
line1 = f" {_Y}⚡{_RS} {_W}Update available:{_RS} {_DM}{current}{_RS} {_C}→{_RS} {_G}{latest}{_RS}"
|
|
162
|
+
cmd_str = "pip install -U akashcli"
|
|
163
|
+
line2 = f" {_DM}Run:{_RS} {_C}{cmd_str}{_RS} {_DM}to update{_RS}"
|
|
164
|
+
|
|
165
|
+
w1 = _cell_width(line1)
|
|
166
|
+
w2 = _cell_width(line2)
|
|
167
|
+
|
|
168
|
+
width = max(w1, w2) + 4
|
|
169
|
+
pad1 = width - w1
|
|
170
|
+
pad2 = width - w2
|
|
171
|
+
|
|
172
|
+
top = f" {_GR}╭{'─' * width}╮{_RS}"
|
|
173
|
+
mid1 = f" {_GR}│{_RS}{line1}{' ' * pad1}{_GR}│{_RS}"
|
|
174
|
+
mid2 = f" {_GR}│{_RS}{line2}{' ' * pad2}{_GR}│{_RS}"
|
|
175
|
+
bot = f" {_GR}╰{'─' * width}╯{_RS}"
|
|
176
|
+
|
|
177
|
+
return f"\n{top}\n{mid1}\n{mid2}\n{bot}\n"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def should_suppress_notice() -> bool:
|
|
181
|
+
"""Check if the notice should be suppressed (e.g. non-tty or json/pipe output)."""
|
|
182
|
+
if os.environ.get("AKASH_NO_UPDATE_CHECK", "").lower() in ("1", "true", "yes"):
|
|
183
|
+
return True
|
|
184
|
+
if not sys.stdout.isatty():
|
|
185
|
+
return True
|
|
186
|
+
# Suppress in quiet or raw data commands
|
|
187
|
+
for arg in sys.argv[1:]:
|
|
188
|
+
if arg in ("--json", "--raw", "--quiet", "-q", "pipe", "stream"):
|
|
189
|
+
return True
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def show_update_warning_if_available():
|
|
194
|
+
"""Atexit / teardown hook: show update warning if available and not yet printed."""
|
|
195
|
+
global _already_printed
|
|
196
|
+
if _already_printed:
|
|
197
|
+
return
|
|
198
|
+
if should_suppress_notice():
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from . import __version__
|
|
203
|
+
available, latest = check_for_updates(__version__, force_sync=False)
|
|
204
|
+
if available and latest:
|
|
205
|
+
_already_printed = True
|
|
206
|
+
print(format_update_box(__version__, latest))
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def mark_update_printed():
|
|
212
|
+
"""Mark update warning as printed so it won't duplicate on exit."""
|
|
213
|
+
global _already_printed
|
|
214
|
+
_already_printed = True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main():
|
|
218
|
+
"""Background worker entry point: python3 -m akashcli.updater"""
|
|
219
|
+
latest = fetch_latest_version(timeout=4.0)
|
|
220
|
+
if latest:
|
|
221
|
+
write_cache(latest)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|