akashcli 4.8.2__tar.gz → 4.8.4__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.8.2 → akashcli-4.8.4}/PKG-INFO +1 -1
  2. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/cli.py +271 -41
  3. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.8.2 → akashcli-4.8.4}/pyproject.toml +1 -1
  5. {akashcli-4.8.2 → akashcli-4.8.4}/README.md +0 -0
  6. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/__init__.py +0 -0
  7. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/_net.py +0 -0
  8. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/auth.py +0 -0
  9. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/client.py +0 -0
  10. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/config.py +0 -0
  11. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/crypto.py +0 -0
  12. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/exec.py +0 -0
  15. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/integrity.py +0 -0
  16. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/logger.py +0 -0
  17. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/models.py +0 -0
  18. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/p2p.py +0 -0
  19. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/packages.py +0 -0
  20. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/relay.py +0 -0
  21. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/restore.py +0 -0
  22. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/storage.py +0 -0
  24. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/terminal.py +0 -0
  25. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/updater.py +0 -0
  26. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/uploader.py +0 -0
  27. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli/utils.py +0 -0
  28. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli.egg-info/SOURCES.txt +0 -0
  29. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli.egg-info/requires.txt +0 -0
  32. {akashcli-4.8.2 → akashcli-4.8.4}/akashcli.egg-info/top_level.txt +0 -0
  33. {akashcli-4.8.2 → akashcli-4.8.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.8.2
3
+ Version: 4.8.4
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
@@ -2774,43 +2774,131 @@ def _render_inspection_card(r: dict) -> None:
2774
2774
 
2775
2775
 
2776
2776
  @agent_app.command("list")
2777
+ @agent_app.command("ls", hidden=True)
2778
+ @agent_app.command("ids", hidden=True)
2779
+ @agent_app.command("profiles", hidden=True)
2777
2780
  def agent_list(raw: bool = typer.Option(False, "--json", help="Raw JSON output")):
2778
- """List all locally saved Agent Profile IDs and their credentials."""
2781
+ """List all locally saved Agent & MCP Profile IDs, credentials, and launch commands."""
2779
2782
  import json
2780
2783
  from pathlib import Path
2784
+
2785
+ is_raw = bool(raw is True)
2781
2786
  agents_file = Path.home() / ".akash" / "agents.json"
2782
2787
  if not agents_file.exists():
2783
- _info("no saved agent profiles found - create one with: akash agent create")
2788
+ _p()
2789
+ _info("No saved agent profiles found. Create one with: " + _cy("akash mcp create -n Dev -r @coder"))
2790
+ _p()
2784
2791
  return
2785
2792
 
2786
2793
  try:
2787
- agents = json.loads(agents_file.read_text())
2794
+ agents = json.loads(agents_file.read_text(encoding="utf-8"))
2788
2795
  except Exception:
2789
2796
  agents = {}
2790
2797
 
2791
2798
  if not agents:
2792
- _info("no saved agent profiles found - create one with: akash agent create")
2799
+ _p()
2800
+ _info("No saved agent profiles found. Create one with: " + _cy("akash mcp create -n Dev -r @coder"))
2801
+ _p()
2793
2802
  return
2794
2803
 
2795
- if raw:
2804
+ if is_raw:
2796
2805
  print(json.dumps(agents, indent=2))
2797
2806
  return
2798
2807
 
2808
+ default_pid = next(reversed(list(agents.keys())))
2809
+
2799
2810
  _p()
2800
- print(f" {_C}◈{_RS} {_W}Available Agent Profile IDs{_RS} {_dim(f'({len(agents)})')}")
2801
- _p()
2802
- print(f" {_DM}{'ID':<6} {'NAME':<20} {'CLIENT ID':<26} MCP LAUNCH COMMAND{_RS}")
2803
- print(f" {_GR}{'─'*75}{_RS}")
2811
+ print(f" {_C}◈{_RS} {_W}Akash MCP Profiles & Agent IDs{_RS} {_dim(f'({len(agents)} configured)')}")
2812
+ print(f" {_GR}{'─'*76}{_RS}")
2813
+
2814
+ hdr_status = f"{'STATUS':<11}"
2815
+ hdr_id = f"{'ID':<8}"
2816
+ hdr_name = f"{'NAME':<16}"
2817
+ hdr_cid = f"{'CLIENT ID SUFFIX':<18}"
2818
+ hdr_scopes = "ROLE / SCOPES"
2819
+ print(f" {_DM}{hdr_status}{hdr_id}{hdr_name}{hdr_cid}{hdr_scopes}{_RS}")
2820
+ print(f" {_GR}{'─'*76}{_RS}")
2821
+
2822
+ import datetime
2823
+ now_utc = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
2824
+
2825
+ last_suffix = ""
2826
+ expired_count = 0
2804
2827
  for pid, data in agents.items():
2805
- name = data.get("name", "Agent")[:19]
2806
- cid = data.get("client_id", "?")
2807
- cid_short = cid[:22] + "…" if len(cid) > 22 else cid
2808
- cmd_hint = f"akash mcp serve --id {pid}"
2809
- print(f" {_ye(pid):<15} {_W}{name:<20}{_RS} {_dim(cid_short):<35} {_C}{cmd_hint}{_RS}")
2810
- _p()
2811
- _info("To launch: akash mcp serve --id <ID>")
2828
+ is_default = (pid == default_pid)
2829
+
2830
+ # Check expiration status
2831
+ is_expired = False
2832
+ exp_val = data.get("expires_at")
2833
+ if exp_val:
2834
+ try:
2835
+ exp_dt = datetime.datetime.fromisoformat(exp_val.replace("Z", "+00:00")).replace(tzinfo=None)
2836
+ if exp_dt < now_utc:
2837
+ is_expired = True
2838
+ except Exception:
2839
+ pass
2840
+
2841
+ if is_expired:
2842
+ expired_count += 1
2843
+ status_txt = "✗ expired"
2844
+ status_color = _R
2845
+ elif is_default:
2846
+ status_txt = "● active"
2847
+ status_color = _G
2848
+ else:
2849
+ status_txt = "○ idle"
2850
+ status_color = _DM
2851
+
2852
+ name = data.get("name", "Agent")
2853
+ if len(name) > 14:
2854
+ name = name[:13] + "…"
2855
+
2856
+ cid = data.get("client_id", "")
2857
+ suffix = f"...{cid[-4:]}" if len(cid) >= 4 else cid
2858
+ if is_default:
2859
+ last_suffix = cid[-4:] if len(cid) >= 4 else cid
2860
+
2861
+ raw_scopes = data.get("scopes", "*")
2862
+ if raw_scopes == "*":
2863
+ scope_desc = "@full (All access)"
2864
+ elif "fs:no_delete" in raw_scopes and "workspace:write" in raw_scopes:
2865
+ scope_desc = "@coder (Safe Dev)"
2866
+ elif "workspace:write" in raw_scopes:
2867
+ scope_desc = "@editor (Code Edit)"
2868
+ elif "exec:read_only" in raw_scopes:
2869
+ scope_desc = "@viewer (Read-Only)"
2870
+ else:
2871
+ scope_desc = raw_scopes.split(",")[0] if raw_scopes else "custom"
2872
+
2873
+ # Safe alignment with ANSI colors
2874
+ c_status = f"{status_color}{status_txt}{_RS}" + " " * max(0, 11 - len(status_txt))
2875
+ c_id = f"{_ye(pid)}{_RS}" + " " * max(0, 8 - len(pid))
2876
+ c_name = f"{_W}{name}{_RS}" + " " * max(0, 16 - len(name))
2877
+ c_cid = f"{_cy(suffix)}{_RS}" + " " * max(0, 18 - len(suffix))
2878
+ c_scopes = f"{_dim(scope_desc)}"
2879
+
2880
+ print(f" {c_status}{c_id}{c_name}{c_cid}{c_scopes}")
2881
+
2882
+ print(f" {_GR}{'─'*76}{_RS}")
2883
+ if expired_count > 0:
2884
+ print(f" {_Y}⚡{_RS} {_W}{expired_count} profile(s) expired.{_RS} {_DM}Run {_cy('akash mcp renew <id>')} to extend or {_cy('akash mcp rm --expired')} to clean up.{_RS}\n")
2885
+ else:
2886
+ _p()
2887
+ print(f" {_W}Quick Launch:{_RS}")
2888
+ print(f" • Use default profile : {_C}akash mcp serve{_RS}")
2889
+ suffix_hint = f" {_dim('(or: --id ' + last_suffix + ')')}" if last_suffix else ""
2890
+ print(f" • Use specific ID : {_C}akash mcp serve --id {default_pid}{_RS}{suffix_hint}")
2891
+ print(f" • Clean idle profiles : {_C}akash mcp rm --all{_RS}")
2892
+ print(f" • Renew expiration : {_C}akash mcp renew {default_pid} --days 30{_RS}")
2893
+ print(f" • Create new profile : {_C}akash mcp create -n Dev -r @coder{_RS}")
2812
2894
  _p()
2813
2895
 
2896
+ @mcp_app.callback(invoke_without_command=True)
2897
+ def mcp_callback(ctx: typer.Context):
2898
+ """Model Context Protocol (MCP) server & AI agent tool bridge."""
2899
+ if ctx.invoked_subcommand is None:
2900
+ agent_list()
2901
+
2814
2902
 
2815
2903
 
2816
2904
  def _resolve_agent_id(target_id: str) -> tuple[str, str, str]:
@@ -2941,6 +3029,12 @@ def agent_info(
2941
3029
  status = d.get("status", "active") + " (live server)"
2942
3030
  exp = d.get("expires_at", "unknown")[:19].replace("T", " ")
2943
3031
  is_live = True
3032
+ if pid in all_a and d.get("expires_at"):
3033
+ all_a[pid]["expires_at"] = d.get("expires_at")
3034
+ try:
3035
+ agents_file.write_text(json.dumps(all_a, indent=2))
3036
+ except Exception:
3037
+ pass
2944
3038
  elif r.status_code == 404:
2945
3039
  status = "not found on server"
2946
3040
  except Exception:
@@ -3015,42 +3109,175 @@ def agent_update(
3015
3109
  else:
3016
3110
  _err(f"Failed to update scopes: {res.text}")
3017
3111
 
3018
- @agent_app.command("revoke")
3019
- def agent_revoke(
3020
- target: Optional[str] = typer.Argument(None, help="Profile ID (e.g. fbf6) or Client ID"),
3021
- id: Optional[str] = typer.Option(None, "--id", help="Profile ID (e.g. fbf6)"),
3022
- url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
3112
+ @agent_app.command("rm")
3113
+ @agent_app.command("remove", hidden=True)
3114
+ @agent_app.command("delete", hidden=True)
3115
+ @agent_app.command("revoke", hidden=True)
3116
+ def agent_remove(
3117
+ target: Optional[str] = typer.Argument(None, help="Profile ID (e.g. 88d7), Client ID suffix (e.g. ddff), or name"),
3118
+ all_idle: bool = typer.Option(False, "--all", "-a", help="Remove all idle profiles, keeping active default"),
3119
+ expired_only: bool = typer.Option(False, "--expired", "-e", help="Remove all expired profiles"),
3120
+ yes: bool = typer.Option(False, "--yes", "-y", "--force", "-f", help="Skip confirmation prompt"),
3121
+ url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
3023
3122
  ):
3024
- """Revoke an agent's access in one command using its short Profile ID."""
3025
- target_id = id or target
3026
- if not target_id:
3027
- _err("Provide a Profile ID (e.g. 'akash agent revoke fbf6')")
3123
+ """Remove / clean up one or more MCP profile IDs locally and on gateway."""
3124
+ import json, datetime
3125
+ from pathlib import Path
3126
+ agents_file = Path.home() / ".akash" / "agents.json"
3127
+ if not agents_file.exists():
3128
+ _info("No saved profiles to remove.")
3129
+ return
3130
+
3131
+ try:
3132
+ agents = json.loads(agents_file.read_text(encoding="utf-8"))
3133
+ except Exception:
3134
+ agents = {}
3135
+
3136
+ if not agents:
3137
+ _info("No saved profiles to remove.")
3138
+ return
3139
+
3140
+ default_pid = next(reversed(list(agents.keys())))
3141
+ now_utc = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
3142
+
3143
+ targets_to_delete = []
3144
+
3145
+ if all_idle:
3146
+ for pid in list(agents.keys()):
3147
+ if pid != default_pid:
3148
+ targets_to_delete.append(pid)
3149
+ if not targets_to_delete:
3150
+ _info("No idle profiles to remove (only active profile exists).")
3151
+ return
3152
+ if not yes:
3153
+ try:
3154
+ sys.stdout.write(f" Remove {len(targets_to_delete)} idle profile(s)? [y/N]: ")
3155
+ sys.stdout.flush()
3156
+ ans = sys.stdin.readline().strip().lower()
3157
+ if ans not in ("y", "yes"):
3158
+ _info("Aborted.")
3159
+ return
3160
+ except (KeyboardInterrupt, EOFError):
3161
+ return
3162
+
3163
+ elif expired_only:
3164
+ for pid, d in list(agents.items()):
3165
+ exp_str = d.get("expires_at")
3166
+ if exp_str:
3167
+ try:
3168
+ exp_dt = datetime.datetime.fromisoformat(exp_str.replace("Z", "+00:00")).replace(tzinfo=None)
3169
+ if exp_dt < now_utc:
3170
+ targets_to_delete.append(pid)
3171
+ except Exception:
3172
+ pass
3173
+ if not targets_to_delete:
3174
+ _info("No expired profiles found to remove.")
3175
+ return
3176
+
3177
+ elif target:
3178
+ pid, cid, name = _resolve_agent_id(target)
3179
+ if pid not in agents:
3180
+ _err(f"Profile '{target}' not found in saved profiles.")
3181
+ raise typer.Exit(1)
3182
+ targets_to_delete.append(pid)
3183
+
3184
+ else:
3185
+ _err("Specify a Profile ID to remove (e.g. 'akash mcp rm 88d7'), or use '--all' / '--expired'.")
3186
+ _info(f"Run {_cy('akash mcp')} to view all IDs.")
3028
3187
  raise typer.Exit(1)
3029
3188
 
3030
- pid, cid, name = _resolve_agent_id(target_id)
3031
- api_key, base = _resolve_client(url, require_key=False, silent=True)
3032
- hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
3033
- if api_key: hdrs["x-api-key"] = api_key
3189
+ _p()
3190
+ removed_count = 0
3191
+ for pid in targets_to_delete:
3192
+ d = agents.get(pid, {})
3193
+ name = d.get("name", "Agent")
3194
+ cid = d.get("client_id", "")
3195
+ suffix = cid[-4:] if len(cid) >= 4 else ""
3196
+
3197
+ # Best-effort remote revocation on gateway
3198
+ target_url = url or d.get("url")
3199
+ _, base = _resolve_client(target_url, require_key=False, silent=True)
3200
+ if cid and base:
3201
+ try:
3202
+ httpx.delete(f"{base}/api/agent/app/{cid}", timeout=3.0)
3203
+ except Exception:
3204
+ pass
3205
+
3206
+ if pid in agents:
3207
+ del agents[pid]
3208
+ removed_count += 1
3209
+ print(f" {_G}✓{_RS} Removed profile {_W}{name}{_RS} ({_ye(pid)}{_dim(' · ...' + suffix if suffix else '')})")
3210
+
3211
+ try:
3212
+ agents_file.write_text(json.dumps(agents, indent=2))
3213
+ except Exception:
3214
+ pass
3034
3215
 
3035
- with _Spin(f"revoking credentials for {pid} ({name})…"):
3036
- res = httpx.delete(f"{base}/api/agent/app/{cid}", headers=hdrs, timeout=10.0)
3216
+ _p()
3217
+ _ok(f"Cleaned up {removed_count} MCP profile(s). {len(agents)} profile(s) remaining.")
3218
+ _p()
3037
3219
 
3038
- # Clean local profile if present
3220
+ @agent_app.command("renew")
3221
+ def agent_renew(
3222
+ target: Optional[str] = typer.Argument(None, help="Profile ID, Client ID suffix, or name (defaults to active profile)"),
3223
+ days: int = typer.Option(7, "--days", "-d", help="Number of days to extend validity (default: 7)"),
3224
+ url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
3225
+ ):
3226
+ """Renew/extend the validity of an MCP profile / agent ID."""
3039
3227
  import json
3040
3228
  from pathlib import Path
3041
3229
  agents_file = Path.home() / ".akash" / "agents.json"
3042
- if agents_file.exists():
3230
+ if not agents_file.exists():
3231
+ _err("No saved profiles found.")
3232
+ raise typer.Exit(1)
3233
+
3234
+ try:
3235
+ agents = json.loads(agents_file.read_text(encoding="utf-8"))
3236
+ except Exception:
3237
+ agents = {}
3238
+
3239
+ if not agents:
3240
+ _err("No saved profiles found.")
3241
+ raise typer.Exit(1)
3242
+
3243
+ target_id = target
3244
+ if not target_id:
3245
+ target_id = next(reversed(list(agents.keys())))
3246
+
3247
+ pid, cid, name = _resolve_agent_id(target_id)
3248
+ cached = agents.get(pid, {})
3249
+ target_url = url or cached.get("url")
3250
+ _, base = _resolve_client(target_url, require_key=False, silent=True)
3251
+
3252
+ with _Spin(f"renewing MCP profile {pid} ({name}) for {days} days…"):
3043
3253
  try:
3044
- agents = json.loads(agents_file.read_text())
3045
- if pid in agents:
3046
- del agents[pid]
3047
- agents_file.write_text(json.dumps(agents, indent=2))
3048
- except Exception: pass
3254
+ res = httpx.patch(
3255
+ f"{base}/api/agent/app/{cid}",
3256
+ json={"renew": True, "ttl_days": days},
3257
+ timeout=8.0
3258
+ )
3259
+ except Exception as e:
3260
+ _err(f"Network error contacting gateway ({base}): {e}")
3261
+ raise typer.Exit(1)
3049
3262
 
3050
3263
  if res.status_code == 200:
3051
- _ok(f"Agent {_ye(pid)} ({name}) revoked successfully!")
3264
+ d = res.json()
3265
+ new_exp = d.get("expires_at", "")
3266
+ if pid in agents:
3267
+ agents[pid]["expires_at"] = new_exp
3268
+ try:
3269
+ agents_file.write_text(json.dumps(agents, indent=2))
3270
+ except Exception:
3271
+ pass
3272
+ _p()
3273
+ _ok(f"Successfully renewed profile {_ye(name)} ({_C}{pid}{_RS} · {_dim('...' + cid[-4:])}) for {_G}{days} days{_RS}!")
3274
+ if new_exp:
3275
+ exp_clean = new_exp[:19].replace("T", " ")
3276
+ print(f" {_DM}New Expiration : {_RS}{_cy(exp_clean)} UTC")
3277
+ _p()
3052
3278
  else:
3053
- _ok(f"Agent {_ye(pid)} removed locally.")
3279
+ _err(f"Failed to renew profile on gateway: HTTP {res.status_code} - {res.text}")
3280
+ raise typer.Exit(1)
3054
3281
 
3055
3282
 
3056
3283
  @agent_app.command("create")
@@ -3101,6 +3328,7 @@ def agent_create(
3101
3328
  "client_secret": d["client_secret"],
3102
3329
  "name": name,
3103
3330
  "scopes": d.get("scopes", effective_scopes),
3331
+ "expires_at": d.get("expires_at"),
3104
3332
  "url": base
3105
3333
  }
3106
3334
  agents_file.write_text(json.dumps(agents, indent=2))
@@ -3603,7 +3831,9 @@ def mcp_serve(
3603
3831
  print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
3604
3832
  print(" " + _DM + "Workspace : " + _RS + _cy(str(Path.cwd())))
3605
3833
  print(" " + _DM + "Gateway : " + _RS + _dim(base))
3606
- print(" " + _DM + "Profile : " + _RS + _ye(pname) + " " + _dim("(" + (profile_id or "custom") + ")"))
3834
+ cid_suffix = f"...{cid[-4:]}" if cid and len(cid) >= 4 else ""
3835
+ suffix_tag = f" · client: {cid_suffix}" if cid_suffix else ""
3836
+ print(" " + _DM + "Profile : " + _RS + _ye(pname) + " " + _dim("(" + (profile_id or "custom") + suffix_tag + ")"))
3607
3837
  print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
3608
3838
  print(" " + _DM + "Active Tools:" + _RS)
3609
3839
  print(" • akash_exec_command " + _dim("(Safe terminal runner)"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.8.2
3
+ Version: 4.8.4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.8.2"
3
+ version = "4.8.4"
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
File without changes
File without changes