akashcli 4.7.9__tar.gz → 4.8.1__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.9 → akashcli-4.8.1}/PKG-INFO +1 -1
  2. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/cli.py +305 -20
  3. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/PKG-INFO +1 -1
  4. {akashcli-4.7.9 → akashcli-4.8.1}/pyproject.toml +1 -1
  5. {akashcli-4.7.9 → akashcli-4.8.1}/README.md +0 -0
  6. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/__init__.py +0 -0
  7. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/_net.py +0 -0
  8. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/auth.py +0 -0
  9. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/client.py +0 -0
  10. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/config.py +0 -0
  11. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/crypto.py +0 -0
  12. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/exec.py +0 -0
  15. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/integrity.py +0 -0
  16. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/logger.py +0 -0
  17. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/models.py +0 -0
  18. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/p2p.py +0 -0
  19. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/packages.py +0 -0
  20. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/relay.py +0 -0
  21. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/restore.py +0 -0
  22. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/snapshot_crypto.py +0 -0
  23. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/storage.py +0 -0
  24. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/terminal.py +0 -0
  25. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/updater.py +0 -0
  26. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/uploader.py +0 -0
  27. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli/utils.py +0 -0
  28. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/SOURCES.txt +0 -0
  29. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/requires.txt +0 -0
  32. {akashcli-4.7.9 → akashcli-4.8.1}/akashcli.egg-info/top_level.txt +0 -0
  33. {akashcli-4.7.9 → akashcli-4.8.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.7.9
3
+ Version: 4.8.1
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
@@ -2854,33 +2854,50 @@ def _resolve_agent_id(target_id: str) -> tuple[str, str, str]:
2854
2854
 
2855
2855
  @agent_app.command("scopes")
2856
2856
  def agent_scopes():
2857
- """View directory of all available roles, scopes, and permission rules with animation."""
2857
+ """View directory of all available roles, presets, scopes, and tools."""
2858
2858
  with _Spin("loading scope directory…"):
2859
2859
  pass
2860
2860
  _p()
2861
- print(f" {_C}◈{_RS} {_W}Akash Agent Scopes & Roles Directory{_RS}")
2861
+ print(f" {_C}◈{_RS} {_W}Akash Agent Scopes & Role Presets Directory{_RS}")
2862
+ print(f" {_GR}────────────────────────────────────────────────────────────{_RS}")
2862
2863
  _p()
2863
- print(f" {_DM}ROLE PRESETS (Recommended):{_RS}")
2864
- print(f" {_cy('@viewer')} {_DM}: Read-Only Inspector (Cannot edit code, delete files, or install packages){_RS}")
2865
- print(f" {_dim('Scopes: workspace:read, exec:read_only, fs:cwd_only')}")
2866
- print(f" {_cy('@coder')} {_DM}: Safe Developer (Can edit code and test; file deletions blocked){_RS}")
2864
+ print(f" {_W}ROLE PRESETS:{_RS}")
2865
+ print(f" {_cy('@viewer')} {_DM}: Read-Only Inspector (Cannot edit code, delete files, or run mutating commands){_RS}")
2866
+ print(f" {_dim('Scopes: workspace:read, exec:read_only, fs:cwd_only, limits:max_commands=50')}")
2867
+ print(f" {_cy('@editor')} {_DM}: Safe Editor (Can create and edit files; cannot run shell or delete){_RS}")
2868
+ print(f" {_dim('Scopes: workspace:read, workspace:write, fs:cwd_only, vault:checkpoint')}")
2869
+ print(f" {_cy('@coder')} {_DM}: Safe Developer (Can edit code, build, and test; file deletions blocked){_RS}")
2867
2870
  print(f" {_dim('Scopes: workspace:read, workspace:write, exec:build, fs:cwd_only, fs:no_delete, vault:checkpoint')}")
2868
2871
  print(f" {_cy('@tester')} {_DM}: QA Runner (Can build and test; cannot edit source files){_RS}")
2869
2872
  print(f" {_dim('Scopes: workspace:read, exec:build, fs:cwd_only, limits:timeout=30')}")
2873
+ print(f" {_cy('@admin')} {_DM}: Full DevOps & Package Manager (Can edit, install packages, network calls){_RS}")
2874
+ print(f" {_dim('Scopes: workspace:read, workspace:write, exec:build, exec:package_install, exec:network, vault:checkpoint')}")
2870
2875
  print(f" {_cy('@full')} {_DM}: Unrestricted Master Access (*){_RS}")
2871
2876
  _p()
2872
- print(f" {_DM}GRANULAR PERMISSION SCOPES:{_RS}")
2873
- print(f" {_ye('exec:read_only')} {_DM}Blocks all mutating commands (touch, mkdir, git commit, sed){_RS}")
2874
- print(f" {_ye('fs:no_delete')} {_DM}Blocks destructive deletions (rm, unlink, git rm){_RS}")
2875
- print(f" {_ye('exec:package_install')} {_DM}Enables installing packages (pip install, npm install){_RS}")
2877
+ print(f" {_W}GRANULAR PERMISSION SCOPES:{_RS}")
2878
+ print(f" {_ye('workspace:read')} {_DM}Enables inspecting files via akash_read_file and akash_list_dir{_RS}")
2879
+ print(f" {_ye('workspace:write')} {_DM}Enables writing files via akash_write_file{_RS}")
2880
+ print(f" {_ye('exec:read_only')} {_DM}Blocks mutating terminal commands (touch, mkdir, sed, git commit){_RS}")
2881
+ print(f" {_ye('fs:no_delete')} {_DM}Blocks destructive deletions (rm, unlink, git rm, rmdir){_RS}")
2882
+ print(f" {_ye('exec:package_install')} {_DM}Enables package managers (pip install, npm install, apt){_RS}")
2876
2883
  print(f" {_ye('exec:network')} {_DM}Enables outbound network pipes (curl, wget){_RS}")
2877
- print(f" {_ye('fs:cwd_only')} {_DM}Restricts operations strictly to current project root{_RS}")
2878
- print(f" {_ye('vault:checkpoint')} {_DM}Enables time-machine undo snapshots before AI edits{_RS}")
2884
+ print(f" {_ye('fs:cwd_only')} {_DM}Restricts operations strictly to current working directory{_RS}")
2885
+ print(f" {_ye('vault:checkpoint')} {_DM}Enables automatic undo rollback snapshots before AI edits{_RS}")
2879
2886
  _p()
2880
- print(f" {_DM}HOW TO ASSIGN OR UPDATE:{_RS}")
2881
- print(f" Create with role : {_C}akash agent create --name 'Agent' --role @coder{_RS}")
2887
+ print(f" {_W}AVAILABLE MCP TOOLS:{_RS}")
2888
+ print(f" • {_C}akash_exec_command{_RS} {_DM}: Safe terminal runner with secret redaction and process timeouts{_RS}")
2889
+ print(f" • {_C}akash_read_file{_RS} {_DM}: Read files with line-range pagination directly into AI context{_RS}")
2890
+ print(f" • {_C}akash_write_file{_RS} {_DM}: Write or update files safely on device{_RS}")
2891
+ print(f" • {_C}akash_list_dir{_RS} {_DM}: Browse directory trees with sizes, types, and depth control{_RS}")
2892
+ print(f" • {_C}akash_checkpoint_create{_RS} {_DM}: Create an encrypted rollback snapshot before major changes{_RS}")
2893
+ print(f" • {_C}akash_undo{_RS} {_DM}: Revert workspace to pre-action state{_RS}")
2894
+ print(f" • {_C}akash_scope_details{_RS} {_DM}: Introspect active permissions and security rules{_RS}")
2895
+ _p()
2896
+ print(f" {_W}HOW TO ASSIGN OR UPDATE:{_RS}")
2897
+ print(f" Create with role : {_C}akash agent create -n 'Dev' -r @coder{_RS}")
2898
+ print(f" Switch role : {_C}akash agent update --id <ID> --role @admin{_RS}")
2882
2899
  print(f" Add a scope : {_C}akash agent update --id <ID> --scopes +exec:package_install{_RS}")
2883
- print(f" Switch role : {_C}akash agent update --id <ID> --role @viewer{_RS}")
2900
+ print(f" Inspect agent : {_C}akash agent info <ID>{_RS}")
2884
2901
  _p()
2885
2902
 
2886
2903
  @agent_app.command("info")
@@ -3220,18 +3237,39 @@ _LAST_UNDO_DIR = Path.home() / ".akash" / "undo_vault"
3220
3237
  def _save_undo_checkpoint():
3221
3238
  try:
3222
3239
  import tarfile
3240
+ cwd = Path.cwd().resolve()
3241
+ home = Path.home().resolve()
3242
+ # Never archive the full home directory or system root!
3243
+ if cwd == home or cwd == Path("/") or str(cwd).startswith(("/proc", "/sys", "/dev")):
3244
+ return
3223
3245
  _LAST_UNDO_DIR.mkdir(parents=True, exist_ok=True)
3224
3246
  undo_tar = _LAST_UNDO_DIR / "workspace_last.tar.gz"
3247
+ excluded = {".git", "venv", "__pycache__", ".akash", "storage", ".termux", "node_modules", ".cache", "build", "dist"}
3225
3248
  with tarfile.open(undo_tar, "w:gz") as tar:
3249
+ count = 0
3226
3250
  for item in os.listdir("."):
3227
- if item in (".git", "venv", "__pycache__", ".akash"):
3251
+ if item in excluded or item.startswith(".") or os.path.islink(item):
3252
+ continue
3253
+ p = Path(item)
3254
+ if not p.exists() or p.is_symlink():
3255
+ continue
3256
+ try:
3257
+ if p.is_file() and p.stat().st_size > 5 * 1024 * 1024:
3258
+ continue
3259
+ except Exception:
3228
3260
  continue
3229
- if os.path.exists(item):
3230
- tar.add(item)
3261
+ tar.add(item, recursive=True, filter=lambda ti: None if (ti.name in excluded or ti.issym()) else ti)
3262
+ count += 1
3263
+ if count > 100:
3264
+ break
3231
3265
  except Exception:
3232
3266
  pass
3233
3267
 
3234
3268
  def _restore_undo_checkpoint() -> tuple[bool, str]:
3269
+ cwd = Path.cwd().resolve()
3270
+ home = Path.home().resolve()
3271
+ if cwd == home or cwd == Path("/"):
3272
+ return False, "Undo snapshot is disabled when running in root or home directory for safety."
3235
3273
  undo_tar = _LAST_UNDO_DIR / "workspace_last.tar.gz"
3236
3274
  if not undo_tar.exists():
3237
3275
  return False, "No undo checkpoint available for this session."
@@ -3342,6 +3380,185 @@ def _parse_temp_duration(val: str) -> int:
3342
3380
  elif unit in ('d', 'day', 'days'): return num * 1440
3343
3381
  return num
3344
3382
 
3383
+ def _handle_tool_read_file(args: dict, scopes: str) -> tuple[str, bool]:
3384
+ path_str = args.get("path", "").strip()
3385
+ if not path_str:
3386
+ return "Error: 'path' argument is required.", True
3387
+
3388
+ target_path = Path(path_str).expanduser()
3389
+ if not target_path.is_absolute():
3390
+ target_path = (Path.cwd() / target_path).resolve()
3391
+ else:
3392
+ target_path = target_path.resolve()
3393
+
3394
+ scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
3395
+ if "*" not in scopes_list and "@full" not in scopes_list:
3396
+ if "fs:cwd_only" in scopes_list:
3397
+ cwd = Path.cwd().resolve()
3398
+ try:
3399
+ target_path.relative_to(cwd)
3400
+ except ValueError:
3401
+ return f"⛔ Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
3402
+
3403
+ if not target_path.exists():
3404
+ return f"Error: File '{path_str}' not found.", True
3405
+ if target_path.is_dir():
3406
+ return f"Error: '{path_str}' is a directory, not a file. Use akash_list_dir to view directory contents.", True
3407
+
3408
+ start_line = int(args.get("start_line", 1) or 1)
3409
+ end_line = args.get("end_line")
3410
+ end_line = int(end_line) if end_line is not None else None
3411
+ max_bytes = int(args.get("max_bytes", 65536) or 65536)
3412
+
3413
+ try:
3414
+ with open(target_path, "rb") as f:
3415
+ chunk = f.read(1024)
3416
+ if b"\x00" in chunk:
3417
+ return f"Binary file detected ({target_path.stat().st_size} bytes). Raw binary cannot be rendered in text context.", True
3418
+
3419
+ with open(target_path, "r", encoding="utf-8", errors="replace") as f:
3420
+ lines = f.readlines()
3421
+
3422
+ total_lines = len(lines)
3423
+ s_idx = max(0, start_line - 1)
3424
+ e_idx = end_line if end_line is not None else total_lines
3425
+ selected_lines = lines[s_idx:e_idx]
3426
+
3427
+ content = "".join(selected_lines)
3428
+ if len(content.encode("utf-8")) > max_bytes:
3429
+ content = content[:max_bytes] + f"\n... [Output truncated at {max_bytes} bytes]"
3430
+
3431
+ rel_name = target_path.name
3432
+ header = f"=== File: {rel_name} (Lines {s_idx + 1}-{min(e_idx, total_lines)} of {total_lines}) ===\n"
3433
+ return header + content, False
3434
+ except Exception as e:
3435
+ return f"Error reading file '{path_str}': {e}", True
3436
+
3437
+ def _handle_tool_write_file(args: dict, scopes: str) -> tuple[str, bool]:
3438
+ path_str = args.get("path", "").strip()
3439
+ content = args.get("content", "")
3440
+ overwrite = args.get("overwrite", True)
3441
+
3442
+ if not path_str:
3443
+ return "Error: 'path' argument is required.", True
3444
+
3445
+ target_path = Path(path_str).expanduser()
3446
+ if not target_path.is_absolute():
3447
+ target_path = (Path.cwd() / target_path).resolve()
3448
+ else:
3449
+ target_path = target_path.resolve()
3450
+
3451
+ scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
3452
+ if "*" not in scopes_list and "@full" not in scopes_list:
3453
+ if "exec:read_only" in scopes_list or "@viewer" in scopes_list:
3454
+ return "⛔ Permission denied: Agent is in read-only mode ('exec:read_only'). File writing is blocked.", True
3455
+ if "fs:cwd_only" in scopes_list:
3456
+ cwd = Path.cwd().resolve()
3457
+ try:
3458
+ target_path.relative_to(cwd)
3459
+ except ValueError:
3460
+ return f"⛔ Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
3461
+
3462
+ if target_path.exists() and not overwrite:
3463
+ return f"Error: File '{path_str}' already exists and overwrite is set to False.", True
3464
+
3465
+ try:
3466
+ target_path.parent.mkdir(parents=True, exist_ok=True)
3467
+ target_path.write_text(content, encoding="utf-8")
3468
+ byte_count = len(content.encode("utf-8"))
3469
+ line_count = len(content.splitlines())
3470
+ return f"✓ File '{path_str}' successfully written ({byte_count} bytes, {line_count} lines).", False
3471
+ except Exception as e:
3472
+ return f"Error writing file '{path_str}': {e}", True
3473
+
3474
+ def _handle_tool_list_dir(args: dict, scopes: str) -> tuple[str, bool]:
3475
+ path_str = args.get("path", ".").strip() or "."
3476
+ depth = min(max(int(args.get("depth", 1) or 1), 1), 3)
3477
+
3478
+ target_path = Path(path_str).expanduser()
3479
+ if not target_path.is_absolute():
3480
+ target_path = (Path.cwd() / target_path).resolve()
3481
+ else:
3482
+ target_path = target_path.resolve()
3483
+
3484
+ scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
3485
+ if "*" not in scopes_list and "@full" not in scopes_list:
3486
+ if "fs:cwd_only" in scopes_list:
3487
+ cwd = Path.cwd().resolve()
3488
+ try:
3489
+ target_path.relative_to(cwd)
3490
+ except ValueError:
3491
+ return f"⛔ Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
3492
+
3493
+ if not target_path.exists():
3494
+ return f"Error: Path '{path_str}' not found.", True
3495
+ if not target_path.is_dir():
3496
+ return f"Error: '{path_str}' is not a directory. Use akash_read_file for files.", True
3497
+
3498
+ try:
3499
+ output_lines = [f"Directory listing of: {target_path}"]
3500
+
3501
+ def _walk(curr_p: Path, cur_depth: int, prefix: str = ""):
3502
+ if cur_depth > depth:
3503
+ return
3504
+ try:
3505
+ entries = sorted(list(curr_p.iterdir()), key=lambda p: (not p.is_dir(), p.name.lower()))
3506
+ except PermissionError:
3507
+ output_lines.append(f"{prefix}└── [Permission Denied]")
3508
+ return
3509
+
3510
+ excluded = {".git", "__pycache__", "venv", ".akash", "node_modules", "storage", ".cache"}
3511
+ filtered = [e for e in entries if e.name not in excluded and not (e.is_symlink() and e.name == "storage")]
3512
+
3513
+ for idx, entry in enumerate(filtered[:100]):
3514
+ is_last = (idx == len(filtered) - 1)
3515
+ ptr = "└── " if is_last else "├── "
3516
+ sub_pfx = " " if is_last else "│ "
3517
+ if entry.is_dir():
3518
+ output_lines.append(f"{prefix}{ptr}📁 {entry.name}/")
3519
+ if cur_depth < depth and not entry.is_symlink():
3520
+ _walk(entry, cur_depth + 1, prefix + sub_pfx)
3521
+ elif entry.is_symlink():
3522
+ output_lines.append(f"{prefix}{ptr}🔗 {entry.name} -> {os.readlink(entry)}")
3523
+ else:
3524
+ try:
3525
+ sz = entry.stat().st_size
3526
+ sz_str = f"{sz / 1024:.1f}KB" if sz >= 1024 else f"{sz}B"
3527
+ except Exception:
3528
+ sz_str = "?"
3529
+ output_lines.append(f"{prefix}{ptr}📄 {entry.name} ({sz_str})")
3530
+ if len(filtered) > 100:
3531
+ output_lines.append(f"{prefix}... [{len(filtered) - 100} more items omitted]")
3532
+
3533
+ _walk(target_path, 1, "")
3534
+ return "\n".join(output_lines), False
3535
+ except Exception as e:
3536
+ return f"Error listing directory '{path_str}': {e}", True
3537
+
3538
+ def _handle_tool_scope_details(scopes: str, profile_name: str, profile_id: str) -> tuple[str, bool]:
3539
+ scopes_list = [s.strip() for s in scopes.split(",") if s.strip()]
3540
+ lines = [
3541
+ f"=== Akash Agent Security Scope Profile ===",
3542
+ f"Profile: {profile_name} (ID: {profile_id})",
3543
+ f"Active Scopes ({len(scopes_list)}): {', '.join(scopes_list)}",
3544
+ "",
3545
+ "Enforced Policies:"
3546
+ ]
3547
+ if "*" in scopes_list or "@full" in scopes_list:
3548
+ lines.append(" • Full Access: No restrictions applied.")
3549
+ else:
3550
+ if "fs:cwd_only" in scopes_list:
3551
+ lines.append(" • Workspace Jailing (fs:cwd_only): Access restricted strictly to current project root; path traversal ('..') is blocked.")
3552
+ if "fs:no_delete" in scopes_list:
3553
+ lines.append(" • Delete Prevention (fs:no_delete): Destructive deletions (rm, rmdir, unlink) are blocked.")
3554
+ if "exec:read_only" in scopes_list or "@viewer" in scopes_list:
3555
+ lines.append(" • Read-Only Mode (exec:read_only): File mutations, file writes, and state changes are blocked.")
3556
+ if "exec:package_install" not in scopes_list:
3557
+ lines.append(" • Package Lock: pip/npm/apt installations are blocked (requires 'exec:package_install').")
3558
+ if "vault:checkpoint" in scopes_list:
3559
+ lines.append(" • Safety Checkpoints (vault:checkpoint): AI modifications create rollback snapshots.")
3560
+ return "\n".join(lines), False
3561
+
3345
3562
  @agent_app.command("serve")
3346
3563
  def mcp_serve(
3347
3564
  url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
@@ -3390,8 +3607,12 @@ def mcp_serve(
3390
3607
  print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
3391
3608
  print(" " + _DM + "Active Tools:" + _RS)
3392
3609
  print(" • akash_exec_command " + _dim("(Safe terminal runner)"))
3610
+ print(" • akash_read_file " + _dim("(Inspect & import files into AI)"))
3611
+ print(" • akash_write_file " + _dim("(Create or edit workspace files)"))
3612
+ print(" • akash_list_dir " + _dim("(Explore directory trees)"))
3393
3613
  print(" • akash_checkpoint_create " + _dim("(Rollback snapshot)"))
3394
3614
  print(" • akash_undo " + _dim("(Revert last AI edit)"))
3615
+ print(" • akash_scope_details " + _dim("(Inspect active session permissions)"))
3395
3616
  print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
3396
3617
  print(" " + _DM + "[LIVE ACTIVITY]" + _RS)
3397
3618
 
@@ -3466,7 +3687,10 @@ def mcp_serve(
3466
3687
  ws.send(json.dumps({
3467
3688
  "type": "tool_response",
3468
3689
  "id": call_id,
3469
- "result": {"content": [{"type": "text", "text": f"⛔ {sec_reason}"}], "isError": True}
3690
+ "result": {
3691
+ "content": [{"type": "text", "text": f"⛔ Security Policy Block: {sec_reason}\nCommand: {cmd}"}],
3692
+ "isError": True
3693
+ }
3470
3694
  }))
3471
3695
  else:
3472
3696
  if any(k in cmd for k in ["touch", "mkdir", "mv", "cp", "sed", ">", ">>", "python", "npm", "pip"]):
@@ -3526,10 +3750,17 @@ def mcp_serve(
3526
3750
  ec_icon = f"{_G}✓{_RS}" if returncode == 0 else f"{_R}✗{_RS}"
3527
3751
  print(f"{ts_pfx} {ec_icon} {ec_color}exec:{_RS} {disp_cmd} {ec_color}[{returncode}]{_RS} {_dim(f'{elapsed_ms:.1f}ms')}")
3528
3752
 
3753
+ if returncode != 0:
3754
+ res_content = bounded_out or f"Command exited with status code {returncode} (no output)"
3755
+ if not res_content.startswith("⛔"):
3756
+ res_content = f"⛔ Command execution failed (exit code {returncode}):\n{res_content}"
3757
+ else:
3758
+ res_content = bounded_out or "(command completed successfully with no output)"
3759
+
3529
3760
  ws.send(json.dumps({
3530
3761
  "type": "tool_response",
3531
3762
  "id": call_id,
3532
- "result": {"content": [{"type": "text", "text": bounded_out or "(no output)"}], "isError": returncode != 0}
3763
+ "result": {"content": [{"type": "text", "text": res_content}], "isError": returncode != 0}
3533
3764
  }))
3534
3765
  elif t_name == "akash_undo":
3535
3766
  with _Spin("undo: reverting workspace to checkpoint", min_seconds=0.25, prefix=ts_pfx):
@@ -3554,6 +3785,60 @@ def mcp_serve(
3554
3785
  "id": call_id,
3555
3786
  "result": {"content": [{"type": "text", "text": f"Checkpoint '{label}' created."}]}
3556
3787
  }))
3788
+
3789
+ elif t_name == "akash_read_file":
3790
+ f_path = args.get("path", "")
3791
+ disp_f = f_path if len(f_path) <= 32 else f_path[:29] + "…"
3792
+ with _Spin(f"read: {disp_f}", min_seconds=0.15, prefix=ts_pfx):
3793
+ res_text, is_err = _handle_tool_read_file(args, cached.get("scopes", "*"))
3794
+ elapsed_ms = (time.monotonic() - t0) * 1000
3795
+ icon = f"{_G}✓{_RS}" if not is_err else f"{_R}✗{_RS}"
3796
+ print(f"{ts_pfx} {icon} {(_G if not is_err else _R)}read:{_RS} {disp_f} {_dim(f'{elapsed_ms:.1f}ms')}")
3797
+ ws.send(json.dumps({
3798
+ "type": "tool_response",
3799
+ "id": call_id,
3800
+ "result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
3801
+ }))
3802
+
3803
+ elif t_name == "akash_write_file":
3804
+ f_path = args.get("path", "")
3805
+ disp_f = f_path if len(f_path) <= 32 else f_path[:29] + "…"
3806
+ _save_undo_checkpoint()
3807
+ with _Spin(f"write: {disp_f}", min_seconds=0.15, prefix=ts_pfx):
3808
+ res_text, is_err = _handle_tool_write_file(args, cached.get("scopes", "*"))
3809
+ elapsed_ms = (time.monotonic() - t0) * 1000
3810
+ icon = f"{_G}✓{_RS}" if not is_err else f"{_R}✗{_RS}"
3811
+ print(f"{ts_pfx} {icon} {(_G if not is_err else _R)}write:{_RS} {disp_f} {_dim(f'{elapsed_ms:.1f}ms')}")
3812
+ ws.send(json.dumps({
3813
+ "type": "tool_response",
3814
+ "id": call_id,
3815
+ "result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
3816
+ }))
3817
+
3818
+ elif t_name == "akash_list_dir":
3819
+ d_path = args.get("path", ".")
3820
+ disp_d = d_path if len(d_path) <= 32 else d_path[:29] + "…"
3821
+ with _Spin(f"list: {disp_d}", min_seconds=0.15, prefix=ts_pfx):
3822
+ res_text, is_err = _handle_tool_list_dir(args, cached.get("scopes", "*"))
3823
+ elapsed_ms = (time.monotonic() - t0) * 1000
3824
+ icon = f"{_G}✓{_RS}" if not is_err else f"{_R}✗{_RS}"
3825
+ print(f"{ts_pfx} {icon} {(_G if not is_err else _R)}list:{_RS} {disp_d} {_dim(f'{elapsed_ms:.1f}ms')}")
3826
+ ws.send(json.dumps({
3827
+ "type": "tool_response",
3828
+ "id": call_id,
3829
+ "result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
3830
+ }))
3831
+
3832
+ elif t_name == "akash_scope_details":
3833
+ with _Spin("inspecting scopes…", min_seconds=0.15, prefix=ts_pfx):
3834
+ res_text, is_err = _handle_tool_scope_details(cached.get("scopes", "*"), pname, profile_id)
3835
+ elapsed_ms = (time.monotonic() - t0) * 1000
3836
+ print(f"{ts_pfx} {_C}🔒 scopes:{_RS} {_dim(pname)} {_dim(f'{elapsed_ms:.1f}ms')}")
3837
+ ws.send(json.dumps({
3838
+ "type": "tool_response",
3839
+ "id": call_id,
3840
+ "result": {"content": [{"type": "text", "text": res_text}], "isError": is_err}
3841
+ }))
3557
3842
  except KeyboardInterrupt:
3558
3843
  print("\n " + _DM + "Worker stopped cleanly." + _RS + "\n")
3559
3844
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.7.9
3
+ Version: 4.8.1
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.7.9"
3
+ version = "4.8.1"
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