akashcli 4.7.1__tar.gz → 4.7.3__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.
Files changed (33) hide show
  1. {akashcli-4.7.1 → akashcli-4.7.3}/PKG-INFO +1 -1
  2. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/__init__.py +1 -1
  3. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/cli.py +97 -6
  4. akashcli-4.7.3/akashcli/updater.py +225 -0
  5. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli.egg-info/PKG-INFO +1 -1
  6. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli.egg-info/SOURCES.txt +1 -0
  7. {akashcli-4.7.1 → akashcli-4.7.3}/pyproject.toml +1 -1
  8. {akashcli-4.7.1 → akashcli-4.7.3}/README.md +0 -0
  9. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/_net.py +0 -0
  10. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/auth.py +0 -0
  11. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/client.py +0 -0
  12. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/config.py +0 -0
  13. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/crypto.py +0 -0
  14. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/env_manager.py +0 -0
  15. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/exceptions.py +0 -0
  16. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/exec.py +0 -0
  17. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/integrity.py +0 -0
  18. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/logger.py +0 -0
  19. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/models.py +0 -0
  20. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/p2p.py +0 -0
  21. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/packages.py +0 -0
  22. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/relay.py +0 -0
  23. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/restore.py +0 -0
  24. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/snapshot_crypto.py +0 -0
  25. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/storage.py +0 -0
  26. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/terminal.py +0 -0
  27. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/uploader.py +0 -0
  28. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli/utils.py +0 -0
  29. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli.egg-info/requires.txt +0 -0
  32. {akashcli-4.7.1 → akashcli-4.7.3}/akashcli.egg-info/top_level.txt +0 -0
  33. {akashcli-4.7.1 → akashcli-4.7.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.7.1
3
+ Version: 4.7.3
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License-Expression: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -2,7 +2,7 @@ try:
2
2
  from importlib.metadata import version
3
3
  __version__ = version("akashcli")
4
4
  except Exception:
5
- __version__ = "4.7.1"
5
+ __version__ = "4.7.3"
6
6
 
7
7
  AKASH_VERSION = __version__
8
8
 
@@ -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():
@@ -241,6 +252,7 @@ def _print_help():
241
252
  f" {C}log{RS} {DM}[--stats]{RS} view operation history & bandwidth summary",
242
253
  f" {C}check{RS} {DM}[--seal]{RS} verify source integrity & tamper detection",
243
254
  f" {C}version{RS} check CLI and server status & versions",
255
+ f" {C}update{RS} check and install latest akashcli release",
244
256
  "",
245
257
  f" {DM}Run {RS}{C}akash <command> --help{RS}{DM} for full options on any command.{RS}",
246
258
  "",
@@ -317,7 +329,50 @@ def version(url: Optional[str] = typer.Option(None, "--url", help="Server URL to
317
329
  print(f" {_DM}server {_RS}{_cy(base)} {_dim(f'(HTTP {resp.status_code})')}")
318
330
  except Exception:
319
331
  print(f" {_DM}server {_RS}{_cy(base)} {_dim('(unreachable)')}")
320
- print()
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()
321
376
 
322
377
  @app.command()
323
378
  def whoami():
@@ -2835,6 +2890,14 @@ def agent_info(
2835
2890
  except Exception:
2836
2891
  status = "offline (server unreachable)"
2837
2892
 
2893
+ csec = cached_profile.get("client_secret")
2894
+ mcp_url = None
2895
+ if csec:
2896
+ import urllib.parse
2897
+ dev_fp = _device_fingerprint()
2898
+ params = urllib.parse.urlencode({"cid": cid, "csec": csec, "dfp": dev_fp})
2899
+ mcp_url = f"{base}/mcp?{params}"
2900
+
2838
2901
  _p()
2839
2902
  print(f" {_C}╭─ 🤖 AGENT PERMISSION DETAILS: {_W}{name}{_RS} {_dim('(' + pid + ')')}")
2840
2903
  print(f" {_GR}│{_RS} {_DM}Profile ID :{_RS} {_ye(pid)}")
@@ -2842,6 +2905,9 @@ def agent_info(
2842
2905
  st_col = _G if "active" in status.lower() else (_Y if "pending" in status.lower() else _R)
2843
2906
  print(f" {_GR}│{_RS} {_DM}Status :{_RS} {st_col}{status.upper()}{_RS}")
2844
2907
  print(f" {_GR}│{_RS} {_DM}Active Scopes :{_RS} {_cy(scopes)}")
2908
+ print(f" {_GR}│{_RS} {_DM}Server URL :{_RS} {_cy(base)}")
2909
+ if mcp_url:
2910
+ print(f" {_GR}│{_RS} {_DM}Connection URL :{_RS} {_dim(mcp_url)}")
2845
2911
  print(f" {_GR}│{_RS}")
2846
2912
  print(f" {_GR}│{_RS} {_DM}PERMISSIONS BREAKDOWN:{_RS}")
2847
2913
  print(f" {_GR}│{_RS} • Code Reading : {_G}[ALLOWED]{_RS}")
@@ -2868,6 +2934,8 @@ def agent_update(
2868
2934
  _err("Provide a Profile ID (e.g. 'akash agent update fbf6 --scopes +exec:package_install')")
2869
2935
  raise typer.Exit(1)
2870
2936
 
2937
+ if role and not role.startswith("@") and role in ("viewer", "coder", "tester", "full"):
2938
+ role = "@" + role
2871
2939
  effective_scopes = scopes if scopes else role
2872
2940
  if not effective_scopes:
2873
2941
  _err("Specify new scopes with --scopes or --role (e.g. --scopes +exec:package_install or --role @coder)")
@@ -2939,6 +3007,13 @@ def agent_create(
2939
3007
  url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
2940
3008
  ):
2941
3009
  """Generate new Client ID and Client Secret with hardware binding."""
3010
+ if role:
3011
+ if not role.startswith("@") and role in ("viewer", "coder", "tester", "full"):
3012
+ role = "@" + role
3013
+ effective_scopes = role
3014
+ else:
3015
+ effective_scopes = scopes
3016
+
2942
3017
  api_key, base = _resolve_client(url, require_key=False)
2943
3018
  hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
2944
3019
  if api_key: hdrs["x-api-key"] = api_key
@@ -2976,18 +3051,34 @@ def agent_create(
2976
3051
  try: agents_file.chmod(0o600)
2977
3052
  except OSError: pass
2978
3053
 
3054
+ cid = d["client_id"]
3055
+ csec = d["client_secret"]
3056
+ dev_fp = _device_fingerprint()
3057
+ import urllib.parse
3058
+ params = urllib.parse.urlencode({"cid": cid, "csec": csec, "dfp": dev_fp})
3059
+ mcp_url = f"{base}/mcp?{params}"
3060
+
2979
3061
  _p()
2980
3062
  _ok(f"Agent Credentials Issued & Saved Locally: {_cy(name)}")
2981
- print(f" {_DM}Profile ID :{_RS} {_C}{profile_id}{_RS}")
2982
- print(f" {_DM}Client ID :{_RS} {_ye(d['client_id'])}")
2983
- print(f" {_DM}Client Secret :{_RS} {_W}{d['client_secret']}{_RS}")
2984
- print(f" {_DM}Expires At :{_RS} {_dim(d['expires_at'][:19].replace('T', ' '))} UTC ({ttl} days)")
3063
+ print(f" {_DM}Profile ID :{_RS} {_C}{profile_id}{_RS}")
3064
+ print(f" {_DM}Client ID :{_RS} {_ye(cid)}")
3065
+ print(f" {_DM}Client Secret :{_RS} {_W}{csec}{_RS}")
3066
+ print(f" {_DM}Server URL :{_RS} {_cy(base)}")
3067
+ print(f" {_DM}Connection URL :{_RS} {_dim(mcp_url)}")
3068
+ print(f" {_DM}Expires At :{_RS} {_dim(d['expires_at'][:19].replace('T', ' '))} UTC ({ttl} days)")
3069
+ _p()
3070
+ print(" " + _C + "Claude Desktop / Cursor" + _RS + " " + _dim("(config.json)") + ":")
3071
+ claude_cfg = json.dumps({"mcpServers": {"akash": {"url": mcp_url}}}, indent=2)
3072
+ for c_line in claude_cfg.splitlines():
3073
+ print(" " + c_line)
2985
3074
  _p()
2986
- _info("To use in Cursor or Claude Desktop, add a Command MCP Server with:")
3075
+ _info("To run local worker or generate temporary share links:")
2987
3076
  if url:
2988
3077
  print(f" {_G}akash mcp serve --id {profile_id} --url {url}{_RS}")
3078
+ print(f" {_DM}akash mcp share {profile_id} --temp 30m --url {url}{_RS}")
2989
3079
  else:
2990
3080
  print(f" {_G}akash mcp serve --id {profile_id}{_RS}")
3081
+ print(f" {_DM}akash mcp share {profile_id} --temp 30m{_RS}")
2991
3082
  _p()
2992
3083
 
2993
3084
  @agent_app.command("requests")
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.7.1
3
+ Version: 4.7.3
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License-Expression: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -20,6 +20,7 @@ akashcli/restore.py
20
20
  akashcli/snapshot_crypto.py
21
21
  akashcli/storage.py
22
22
  akashcli/terminal.py
23
+ akashcli/updater.py
23
24
  akashcli/uploader.py
24
25
  akashcli/utils.py
25
26
  akashcli.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.7.1"
3
+ version = "4.7.3"
4
4
  description = "Akash CLI — secure remote shell relay, file vault, and device management"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
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