akashcli 4.8.3__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.3 → akashcli-4.8.4}/PKG-INFO +1 -1
  2. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/cli.py +196 -28
  3. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.8.3 → akashcli-4.8.4}/pyproject.toml +1 -1
  5. {akashcli-4.8.3 → akashcli-4.8.4}/README.md +0 -0
  6. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/__init__.py +0 -0
  7. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/_net.py +0 -0
  8. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/auth.py +0 -0
  9. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/client.py +0 -0
  10. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/config.py +0 -0
  11. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/crypto.py +0 -0
  12. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/exec.py +0 -0
  15. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/integrity.py +0 -0
  16. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/logger.py +0 -0
  17. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/models.py +0 -0
  18. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/p2p.py +0 -0
  19. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/packages.py +0 -0
  20. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/relay.py +0 -0
  21. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/restore.py +0 -0
  22. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/storage.py +0 -0
  24. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/terminal.py +0 -0
  25. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/updater.py +0 -0
  26. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/uploader.py +0 -0
  27. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli/utils.py +0 -0
  28. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli.egg-info/SOURCES.txt +0 -0
  29. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli.egg-info/requires.txt +0 -0
  32. {akashcli-4.8.3 → akashcli-4.8.4}/akashcli.egg-info/top_level.txt +0 -0
  33. {akashcli-4.8.3 → 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.3
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
@@ -2819,11 +2819,35 @@ def agent_list(raw: bool = typer.Option(False, "--json", help="Raw JSON output")
2819
2819
  print(f" {_DM}{hdr_status}{hdr_id}{hdr_name}{hdr_cid}{hdr_scopes}{_RS}")
2820
2820
  print(f" {_GR}{'─'*76}{_RS}")
2821
2821
 
2822
+ import datetime
2823
+ now_utc = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
2824
+
2822
2825
  last_suffix = ""
2826
+ expired_count = 0
2823
2827
  for pid, data in agents.items():
2824
2828
  is_default = (pid == default_pid)
2825
- status_txt = "● active" if is_default else "○ idle"
2826
- status_color = _G if is_default else _DM
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
2827
2851
 
2828
2852
  name = data.get("name", "Agent")
2829
2853
  if len(name) > 14:
@@ -2856,12 +2880,16 @@ def agent_list(raw: bool = typer.Option(False, "--json", help="Raw JSON output")
2856
2880
  print(f" {c_status}{c_id}{c_name}{c_cid}{c_scopes}")
2857
2881
 
2858
2882
  print(f" {_GR}{'─'*76}{_RS}")
2859
- _p()
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()
2860
2887
  print(f" {_W}Quick Launch:{_RS}")
2861
2888
  print(f" • Use default profile : {_C}akash mcp serve{_RS}")
2862
2889
  suffix_hint = f" {_dim('(or: --id ' + last_suffix + ')')}" if last_suffix else ""
2863
2890
  print(f" • Use specific ID : {_C}akash mcp serve --id {default_pid}{_RS}{suffix_hint}")
2864
- print(f" • Inspect permissions : {_C}akash mcp info {default_pid}{_RS}")
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}")
2865
2893
  print(f" • Create new profile : {_C}akash mcp create -n Dev -r @coder{_RS}")
2866
2894
  _p()
2867
2895
 
@@ -3001,6 +3029,12 @@ def agent_info(
3001
3029
  status = d.get("status", "active") + " (live server)"
3002
3030
  exp = d.get("expires_at", "unknown")[:19].replace("T", " ")
3003
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
3004
3038
  elif r.status_code == 404:
3005
3039
  status = "not found on server"
3006
3040
  except Exception:
@@ -3075,42 +3109,175 @@ def agent_update(
3075
3109
  else:
3076
3110
  _err(f"Failed to update scopes: {res.text}")
3077
3111
 
3078
- @agent_app.command("revoke")
3079
- def agent_revoke(
3080
- target: Optional[str] = typer.Argument(None, help="Profile ID (e.g. fbf6) or Client ID"),
3081
- id: Optional[str] = typer.Option(None, "--id", help="Profile ID (e.g. fbf6)"),
3082
- 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"),
3083
3122
  ):
3084
- """Revoke an agent's access in one command using its short Profile ID."""
3085
- target_id = id or target
3086
- if not target_id:
3087
- _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.")
3088
3187
  raise typer.Exit(1)
3089
3188
 
3090
- pid, cid, name = _resolve_agent_id(target_id)
3091
- api_key, base = _resolve_client(url, require_key=False, silent=True)
3092
- hdrs = {"Origin": "https://akash.cli", "x-device-id": _device_fingerprint()}
3093
- 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
3094
3215
 
3095
- with _Spin(f"revoking credentials for {pid} ({name})…"):
3096
- 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()
3097
3219
 
3098
- # 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."""
3099
3227
  import json
3100
3228
  from pathlib import Path
3101
3229
  agents_file = Path.home() / ".akash" / "agents.json"
3102
- 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…"):
3103
3253
  try:
3104
- agents = json.loads(agents_file.read_text())
3105
- if pid in agents:
3106
- del agents[pid]
3107
- agents_file.write_text(json.dumps(agents, indent=2))
3108
- 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)
3109
3262
 
3110
3263
  if res.status_code == 200:
3111
- _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()
3112
3278
  else:
3113
- _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)
3114
3281
 
3115
3282
 
3116
3283
  @agent_app.command("create")
@@ -3161,6 +3328,7 @@ def agent_create(
3161
3328
  "client_secret": d["client_secret"],
3162
3329
  "name": name,
3163
3330
  "scopes": d.get("scopes", effective_scopes),
3331
+ "expires_at": d.get("expires_at"),
3164
3332
  "url": base
3165
3333
  }
3166
3334
  agents_file.write_text(json.dumps(agents, indent=2))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.8.3
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.3"
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