akashcli 4.8.7__tar.gz → 4.8.9__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.8.7 → akashcli-4.8.9}/PKG-INFO +1 -1
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/__init__.py +1 -1
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/cli.py +333 -43
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.8.7 → akashcli-4.8.9}/pyproject.toml +1 -1
- {akashcli-4.8.7 → akashcli-4.8.9}/README.md +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/_net.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/auth.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/client.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/config.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/crypto.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/env_manager.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/exceptions.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/exec.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/integrity.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/logger.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/models.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/p2p.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/packages.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/relay.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/restore.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/storage.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/terminal.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/updater.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/uploader.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli/utils.py +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.8.7 → akashcli-4.8.9}/setup.cfg +0 -0
|
@@ -136,7 +136,7 @@ def _resolve_client(url_override: str = None, require_key: bool = False, silent:
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
_GUEST_WARNING = (
|
|
139
|
-
f" {_Y}
|
|
139
|
+
f" {_Y}!{_RS} {_Y}Running as guest{_RS} - not logged in.\n"
|
|
140
140
|
f" {_DM}Features are limited and your data won't be saved to the cloud.\n"
|
|
141
141
|
f" Run {_RS}{_C}akash login{_RS}{_DM} to unlock full access.{_RS}"
|
|
142
142
|
)
|
|
@@ -243,7 +243,7 @@ def _print_help():
|
|
|
243
243
|
f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or manage enrolled devices",
|
|
244
244
|
"",
|
|
245
245
|
f" {DM}AI AGENTS & MCP{RS}",
|
|
246
|
-
f" {C}mcp{RS} {DM}<serve|share|create|list…>{RS} AI agent & MCP tool bridge (
|
|
246
|
+
f" {C}mcp{RS} {DM}<serve|share|create|list…>{RS} AI agent & MCP tool bridge (Cursor, Claude, ChatGPT)",
|
|
247
247
|
"",
|
|
248
248
|
f" {DM}FILES & CLOUD VAULT{RS}",
|
|
249
249
|
f" {C}cloud{RS} {DM}<upload|files|stream|delete…>{RS} cloud file vault & media streaming engine",
|
|
@@ -359,7 +359,7 @@ def update():
|
|
|
359
359
|
_p()
|
|
360
360
|
return
|
|
361
361
|
|
|
362
|
-
print(f" {_Y}
|
|
362
|
+
print(f" {_Y}◈{_RS} New version available: {_DM}{__version__}{_RS} {_C}→{_RS} {_G}{latest}{_RS}")
|
|
363
363
|
with _Spin(f"updating akashcli to v{latest}…"):
|
|
364
364
|
cmd = [sys.executable, "-m", "pip", "install", "-U", "--no-cache-dir", f"akashcli=={latest}", "--break-system-packages"]
|
|
365
365
|
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
@@ -464,10 +464,10 @@ def cmd_url(
|
|
|
464
464
|
_info("To reset URL : akash url --reset")
|
|
465
465
|
_p()
|
|
466
466
|
def _storage_discontinued_notice():
|
|
467
|
-
print(f"\n{_R}
|
|
467
|
+
print(f"\n{_R}!️ [SERVICE DISCONTINUED] Cloud storage & upload services are disabled.{_RS}")
|
|
468
468
|
print(f" {_DM}Public cloud uploading has been discontinued due to lack of funds and hosting costs.{_RS}")
|
|
469
469
|
print(f" {_DM}We are actively seeking sponsors to help restore community storage infrastructure.{_RS}\n")
|
|
470
|
-
print(f" {_C}
|
|
470
|
+
print(f" {_C}Sponsoring the project:{_RS}")
|
|
471
471
|
print(f" Run {_YE}akash sponsor{_RS} or visit {_CY}https://github.com/juniorsir/akashcli#sponsorship{_RS}\n")
|
|
472
472
|
raise typer.Exit(1)
|
|
473
473
|
|
|
@@ -476,7 +476,7 @@ def _storage_discontinued_notice():
|
|
|
476
476
|
def cmd_sponsor():
|
|
477
477
|
"""Information on sponsoring Akash CLI to restore storage services."""
|
|
478
478
|
print(f"\n{_C}╔═══════════════════════════════════════════════════════════════╗{_RS}")
|
|
479
|
-
print(f"{_C}║
|
|
479
|
+
print(f"{_C}║ SPONSOR AKASH CLI INFRASTRUCTURE ║{_RS}")
|
|
480
480
|
print(f"{_C}╚═══════════════════════════════════════════════════════════════╝{_RS}\n")
|
|
481
481
|
print(f" {_B}Status Update:{_RS}")
|
|
482
482
|
print(f" Public cloud storage and file uploading services have been")
|
|
@@ -692,7 +692,7 @@ def _interactive_explorer(vfs, root_name="/"):
|
|
|
692
692
|
sys.stdout.write(f" {_GR}────────────────────────────────────────────────────────────{_RS}\r\n")
|
|
693
693
|
|
|
694
694
|
full_path = "".join([n[1] + ("/" if n[1] != "/" and not n[1].endswith("/") else "") for n in path_stack])
|
|
695
|
-
sys.stdout.write(f"
|
|
695
|
+
sys.stdout.write(f" [DIR] {_C}{full_path}{_RS}\r\n")
|
|
696
696
|
sys.stdout.write(f" {_GR}────────────────────────────────────────────────────────────{_RS}\r\n")
|
|
697
697
|
|
|
698
698
|
if not keys:
|
|
@@ -704,7 +704,7 @@ def _interactive_explorer(vfs, root_name="/"):
|
|
|
704
704
|
child = children[k]
|
|
705
705
|
is_sel = (actual_idx == selected_idx)
|
|
706
706
|
cursor = f"{_G}> {_RS}" if is_sel else " "
|
|
707
|
-
icon = "
|
|
707
|
+
icon = "[DIR]" if child["type"] == "dir" else "[FILE]"
|
|
708
708
|
|
|
709
709
|
display_name = k[:35] + ("…" if len(k) > 35 else "")
|
|
710
710
|
if child["type"] == "dir":
|
|
@@ -2704,11 +2704,11 @@ def env_pull(
|
|
|
2704
2704
|
|
|
2705
2705
|
mcp_app = typer.Typer(help="Model Context Protocol (MCP) server & AI agent tool bridge.")
|
|
2706
2706
|
agent_app = mcp_app
|
|
2707
|
-
app.add_typer(mcp_app, name="mcp", help="MCP server · AI agent tools · Claude/Cursor/
|
|
2707
|
+
app.add_typer(mcp_app, name="mcp", help="MCP server · AI agent tools · Claude/Cursor/ChatGPT bridge")
|
|
2708
2708
|
app.add_typer(mcp_app, name="agent", hidden=True, help="Alias for akash mcp")
|
|
2709
2709
|
|
|
2710
2710
|
def _render_inspection_card(r: dict) -> None:
|
|
2711
|
-
print(f"\n {_C}╭─
|
|
2711
|
+
print(f"\n {_C}╭─ ◈ INCOMING AGENT DEVICE REGISTRATION ────────────────────────────{_RS}")
|
|
2712
2712
|
print(f" {_GR}│{_RS} {_DM}Request ID :{_RS} {_ye(r['request_id'])}")
|
|
2713
2713
|
print(f" {_GR}│{_RS} {_DM}Client Name :{_RS} {_W}{r['client_name']}{_RS}")
|
|
2714
2714
|
print(f" {_GR}│{_RS} {_DM}Client ID :{_RS} {_dim(r['client_id'])}")
|
|
@@ -2868,7 +2868,7 @@ def agent_list(raw: bool = typer.Option(False, "--json", help="Raw JSON output")
|
|
|
2868
2868
|
|
|
2869
2869
|
print(f" {_GR}{'─'*76}{_RS}")
|
|
2870
2870
|
if expired_count > 0:
|
|
2871
|
-
print(f" {_Y}
|
|
2871
|
+
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")
|
|
2872
2872
|
else:
|
|
2873
2873
|
_p()
|
|
2874
2874
|
print(f" {_W}Quick Launch:{_RS}")
|
|
@@ -3036,7 +3036,7 @@ def agent_info(
|
|
|
3036
3036
|
mcp_url = f"{base}/mcp?{params}"
|
|
3037
3037
|
|
|
3038
3038
|
_p()
|
|
3039
|
-
print(f" {_C}╭─
|
|
3039
|
+
print(f" {_C}╭─ AGENT PERMISSION DETAILS: {_W}{name}{_RS} {_dim('(' + pid + ')')}")
|
|
3040
3040
|
print(f" {_GR}│{_RS} {_DM}Profile ID :{_RS} {_ye(pid)}")
|
|
3041
3041
|
print(f" {_GR}│{_RS} {_DM}Client ID :{_RS} {_dim(cid)}")
|
|
3042
3042
|
st_col = _G if "active" in status.lower() else (_Y if "pending" in status.lower() else _R)
|
|
@@ -3614,7 +3614,7 @@ def _handle_tool_read_file(args: dict, scopes: str) -> tuple[str, bool]:
|
|
|
3614
3614
|
try:
|
|
3615
3615
|
target_path.relative_to(cwd)
|
|
3616
3616
|
except ValueError:
|
|
3617
|
-
return f"
|
|
3617
|
+
return f"[BLOCKED] Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
|
|
3618
3618
|
|
|
3619
3619
|
if not target_path.exists():
|
|
3620
3620
|
return f"Error: File '{path_str}' not found.", True
|
|
@@ -3667,13 +3667,13 @@ def _handle_tool_write_file(args: dict, scopes: str) -> tuple[str, bool]:
|
|
|
3667
3667
|
scopes_list = [s.strip().lower() for s in scopes.split(",") if s.strip()]
|
|
3668
3668
|
if "*" not in scopes_list and "@full" not in scopes_list:
|
|
3669
3669
|
if "exec:read_only" in scopes_list or "@viewer" in scopes_list:
|
|
3670
|
-
return "
|
|
3670
|
+
return "[BLOCKED] Permission denied: Agent is in read-only mode ('exec:read_only'). File writing is blocked.", True
|
|
3671
3671
|
if "fs:cwd_only" in scopes_list:
|
|
3672
3672
|
cwd = Path.cwd().resolve()
|
|
3673
3673
|
try:
|
|
3674
3674
|
target_path.relative_to(cwd)
|
|
3675
3675
|
except ValueError:
|
|
3676
|
-
return f"
|
|
3676
|
+
return f"[BLOCKED] Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
|
|
3677
3677
|
|
|
3678
3678
|
if target_path.exists() and not overwrite:
|
|
3679
3679
|
return f"Error: File '{path_str}' already exists and overwrite is set to False.", True
|
|
@@ -3704,7 +3704,7 @@ def _handle_tool_list_dir(args: dict, scopes: str) -> tuple[str, bool]:
|
|
|
3704
3704
|
try:
|
|
3705
3705
|
target_path.relative_to(cwd)
|
|
3706
3706
|
except ValueError:
|
|
3707
|
-
return f"
|
|
3707
|
+
return f"[BLOCKED] Permission denied by scope 'fs:cwd_only': Path '{path_str}' is outside working directory ({cwd}).", True
|
|
3708
3708
|
|
|
3709
3709
|
if not target_path.exists():
|
|
3710
3710
|
return f"Error: Path '{path_str}' not found.", True
|
|
@@ -3731,18 +3731,18 @@ def _handle_tool_list_dir(args: dict, scopes: str) -> tuple[str, bool]:
|
|
|
3731
3731
|
ptr = "└── " if is_last else "├── "
|
|
3732
3732
|
sub_pfx = " " if is_last else "│ "
|
|
3733
3733
|
if entry.is_dir():
|
|
3734
|
-
output_lines.append(f"{prefix}{ptr}
|
|
3734
|
+
output_lines.append(f"{prefix}{ptr}[DIR] {entry.name}/")
|
|
3735
3735
|
if cur_depth < depth and not entry.is_symlink():
|
|
3736
3736
|
_walk(entry, cur_depth + 1, prefix + sub_pfx)
|
|
3737
3737
|
elif entry.is_symlink():
|
|
3738
|
-
output_lines.append(f"{prefix}{ptr}
|
|
3738
|
+
output_lines.append(f"{prefix}{ptr}[LINK] {entry.name} -> {os.readlink(entry)}")
|
|
3739
3739
|
else:
|
|
3740
3740
|
try:
|
|
3741
3741
|
sz = entry.stat().st_size
|
|
3742
3742
|
sz_str = f"{sz / 1024:.1f}KB" if sz >= 1024 else f"{sz}B"
|
|
3743
3743
|
except Exception:
|
|
3744
3744
|
sz_str = "?"
|
|
3745
|
-
output_lines.append(f"{prefix}{ptr}
|
|
3745
|
+
output_lines.append(f"{prefix}{ptr}[FILE] {entry.name} ({sz_str})")
|
|
3746
3746
|
if len(filtered) > 100:
|
|
3747
3747
|
output_lines.append(f"{prefix}... [{len(filtered) - 100} more items omitted]")
|
|
3748
3748
|
|
|
@@ -3852,7 +3852,7 @@ def mcp_serve(
|
|
|
3852
3852
|
if ack.get("type") == "pending_approval":
|
|
3853
3853
|
req_id = ack.get("request_id", "?")
|
|
3854
3854
|
sys.stdout.write(chr(13) + chr(27) + "[2K"); sys.stdout.flush()
|
|
3855
|
-
print("\n " + _Y + "
|
|
3855
|
+
print("\n " + _Y + "◈" + _RS + " " + _W + "New device registration pending:" + _RS)
|
|
3856
3856
|
print(" " + _DM + "Request ID : " + _RS + _ye(req_id))
|
|
3857
3857
|
print(" " + _DM + "Device FP : " + _RS + _dim(dev_fp))
|
|
3858
3858
|
print()
|
|
@@ -3908,7 +3908,7 @@ def mcp_serve(
|
|
|
3908
3908
|
"type": "tool_response",
|
|
3909
3909
|
"id": call_id,
|
|
3910
3910
|
"result": {
|
|
3911
|
-
"content": [{"type": "text", "text": f"
|
|
3911
|
+
"content": [{"type": "text", "text": f"[BLOCKED] Security Policy Block: {sec_reason}\nCommand: {cmd}"}],
|
|
3912
3912
|
"isError": True
|
|
3913
3913
|
}
|
|
3914
3914
|
}))
|
|
@@ -3957,11 +3957,11 @@ def mcp_serve(
|
|
|
3957
3957
|
except Exception:
|
|
3958
3958
|
raw_out = ""
|
|
3959
3959
|
elapsed_ms = 25000.0
|
|
3960
|
-
raw_out = raw_out or "
|
|
3960
|
+
raw_out = raw_out or "[BLOCKED] Command timed out after 25 seconds."
|
|
3961
3961
|
returncode = 124
|
|
3962
3962
|
except Exception as cmd_err:
|
|
3963
3963
|
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
3964
|
-
raw_out = f"
|
|
3964
|
+
raw_out = f"[BLOCKED] Command execution error: {cmd_err}"
|
|
3965
3965
|
returncode = 1
|
|
3966
3966
|
|
|
3967
3967
|
bounded_out = _smart_truncate_output(raw_out)
|
|
@@ -3973,8 +3973,8 @@ def mcp_serve(
|
|
|
3973
3973
|
|
|
3974
3974
|
if returncode != 0:
|
|
3975
3975
|
res_content = bounded_out or f"Command exited with status code {returncode} (no output)"
|
|
3976
|
-
if not res_content.startswith("
|
|
3977
|
-
res_content = f"
|
|
3976
|
+
if not res_content.startswith("[BLOCKED]"):
|
|
3977
|
+
res_content = f"[BLOCKED] Command execution failed (exit code {returncode}):\n{res_content}"
|
|
3978
3978
|
else:
|
|
3979
3979
|
res_content = bounded_out or "(command completed successfully with no output)"
|
|
3980
3980
|
|
|
@@ -4081,7 +4081,7 @@ def mcp_serve(
|
|
|
4081
4081
|
reconnect_attempts += 1
|
|
4082
4082
|
backoff = min(1.2 * (1.5 ** min(reconnect_attempts, 6)), 30.0) + random.uniform(0.1, 0.4)
|
|
4083
4083
|
now_t = datetime.datetime.now().strftime("%H:%M:%S")
|
|
4084
|
-
print(f" {_GR}{now_t}{_RS} {_Y}
|
|
4084
|
+
print(f" {_GR}{now_t}{_RS} {_Y}!{_RS} {_dim(f'Worker disconnected ({e}). Reconnecting in {backoff:.1f}s (attempt {reconnect_attempts})…')}")
|
|
4085
4085
|
time.sleep(backoff)
|
|
4086
4086
|
|
|
4087
4087
|
@agent_app.command("share")
|
|
@@ -4090,6 +4090,7 @@ def mcp_share(
|
|
|
4090
4090
|
target: Optional[str] = typer.Argument(None, help="Profile ID (optional if only 1 profile exists)"),
|
|
4091
4091
|
id: Optional[str] = typer.Option(None, "--id", "-i", help="Profile ID"),
|
|
4092
4092
|
temp: Optional[str] = typer.Option(None, "--temp", "-t", help="Temporary short URL duration (e.g. '30m', '30 min', '1h')"),
|
|
4093
|
+
local: bool = typer.Option(False, "--local", "-l", help="Show snippet only for local-first zero-network stdio MCP"),
|
|
4093
4094
|
codex: bool = typer.Option(False, "--codex", "-c", help="Show snippet only for Codex CLI/Desktop"),
|
|
4094
4095
|
claude: bool = typer.Option(False, "--claude", help="Show snippet only for Claude Desktop / Cursor"),
|
|
4095
4096
|
chatgpt: bool = typer.Option(False, "--chatgpt", "-g", help="Show snippet only for ChatGPT Custom GPT Actions"),
|
|
@@ -4250,16 +4251,48 @@ def mcp_share(
|
|
|
4250
4251
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4251
4252
|
_p()
|
|
4252
4253
|
|
|
4253
|
-
show_all = not codex and not claude and not chatgpt
|
|
4254
|
+
show_all = not local and not codex and not claude and not chatgpt
|
|
4255
|
+
|
|
4256
|
+
# ── 1. LOCAL-FIRST STDIO MODE (ZERO NETWORK) ──
|
|
4257
|
+
if show_all or local:
|
|
4258
|
+
print(" " + _C + "1. Local Mode (Offline · Zero Network · Direct Stdio · Recommended)" + _RS + ":")
|
|
4259
|
+
print(" " + _DM + "For Claude Desktop, Cursor & local AI agents on this device." + _RS)
|
|
4260
|
+
print(" " + _dim(" (Zero network exposure, no open ports, direct local pipe)"))
|
|
4261
|
+
stdio_cfg = json.dumps({
|
|
4262
|
+
"mcpServers": {
|
|
4263
|
+
"akash": {
|
|
4264
|
+
"command": "akash",
|
|
4265
|
+
"args": ["mcp", "stdio"]
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
}, indent=2)
|
|
4269
|
+
for c_line in stdio_cfg.splitlines():
|
|
4270
|
+
print(" " + c_line)
|
|
4271
|
+
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4272
|
+
_p()
|
|
4254
4273
|
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
print(" " +
|
|
4274
|
+
# ── 2. NETWORK / RELAY MODE (AUTHENTICATED OVER DOMAIN / CLOUDFLARE / AKASHBOX) ──
|
|
4275
|
+
if show_all or claude:
|
|
4276
|
+
is_custom = bool(base != DEFAULT_STORAGE_URL)
|
|
4277
|
+
domain_tag = "Custom Domain / Cloudflare" if is_custom else "Akashbox Network Relay"
|
|
4278
|
+
print(" " + _C + "2. Network / Internet Mode (" + domain_tag + ")" + _RS + ":")
|
|
4279
|
+
print(" " + _DM + "Active Domain : " + _RS + _cy(base))
|
|
4280
|
+
print(" " + _DM + "Hardware Auth : " + _RS + _G + "Strict (Client ID + Secret + Device Fingerprint required)" + _RS)
|
|
4281
|
+
print(" " + _DM + "Authenticated URL : " + _RS)
|
|
4282
|
+
print(" " + _ye(local_url))
|
|
4283
|
+
_p()
|
|
4284
|
+
print(" " + _DM + "Config snippet for remote Claude Desktop, Cursor & Cline:" + _RS)
|
|
4285
|
+
claude_cfg = json.dumps({"mcpServers": {"akash": {"url": local_url}}}, indent=2)
|
|
4286
|
+
for c_line in claude_cfg.splitlines():
|
|
4287
|
+
print(" " + c_line)
|
|
4288
|
+
print(" " + _DM + " Logo / Icon: Clean Cloud Icon delivered automatically via MCP handshake" + _RS)
|
|
4289
|
+
_p()
|
|
4290
|
+
print(" " + _dim(" • To use a custom domain or Cloudflare tunnel: akash url https://your-domain.com"))
|
|
4291
|
+
print(" " + _dim(" • To reset back to default Akashbox relay : akash url --reset"))
|
|
4260
4292
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4261
4293
|
_p()
|
|
4262
4294
|
|
|
4295
|
+
# ── 3. CHATGPT CUSTOM GPT ACTIONS (OPENAPI REST) ──
|
|
4263
4296
|
if show_all or chatgpt:
|
|
4264
4297
|
app_token = None
|
|
4265
4298
|
for tok_path in [Path.home() / ".akash_app_tokens.json", Path.home() / ".akash" / "app_tokens.json"]:
|
|
@@ -4282,7 +4315,7 @@ def mcp_share(
|
|
|
4282
4315
|
token_val = app_token or csec or "akash_sec_client_03e804cabbbcface799a4522ca673dd166b8e76813f84389"
|
|
4283
4316
|
openapi_url = base + "/openapi.json"
|
|
4284
4317
|
privacy_url = base + "/privacy"
|
|
4285
|
-
print(" " + _C + "For ChatGPT Custom GPT (OpenAPI Actions)" + _RS + ":")
|
|
4318
|
+
print(" " + _C + "3. For ChatGPT Custom GPT (OpenAPI Actions)" + _RS + ":")
|
|
4286
4319
|
print(" " + _DM + "OpenAPI Schema URL : " + _RS + _cy(openapi_url))
|
|
4287
4320
|
print(" " + _DM + "Authentication : " + _RS + _W + "API Key" + _RS + " " + _dim("->") + " " + _W + "Custom" + _RS)
|
|
4288
4321
|
print(" " + _DM + "Custom Header Name : " + _RS + _ye("X-App-Token"))
|
|
@@ -4292,7 +4325,7 @@ def mcp_share(
|
|
|
4292
4325
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4293
4326
|
_p()
|
|
4294
4327
|
|
|
4295
|
-
if
|
|
4328
|
+
if codex:
|
|
4296
4329
|
print(" " + _C + "For Codex CLI / Desktop" + _RS + " " + _dim("(~/.codex/config.toml)") + ":")
|
|
4297
4330
|
print(" " + _DM + "Option A: Same Device (Termux / Localhost)" + _RS)
|
|
4298
4331
|
print(" [mcp_servers.akash]")
|
|
@@ -4305,15 +4338,6 @@ def mcp_share(
|
|
|
4305
4338
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4306
4339
|
_p()
|
|
4307
4340
|
|
|
4308
|
-
if show_all or claude:
|
|
4309
|
-
print(" " + _C + "For Claude Desktop, Cursor & Cline" + _RS + " " + _dim("(config.json / settings.json)") + ":")
|
|
4310
|
-
claude_cfg = json.dumps({"mcpServers": {"akash": {"url": local_url}}}, indent=2)
|
|
4311
|
-
for c_line in claude_cfg.splitlines():
|
|
4312
|
-
print(" " + c_line)
|
|
4313
|
-
print(" " + _DM + " Logo / Icon: Clean Cloud Icon delivered automatically via MCP handshake" + _RS)
|
|
4314
|
-
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4315
|
-
_p()
|
|
4316
|
-
|
|
4317
4341
|
_info("Paste snippet into AI client, then keep running:")
|
|
4318
4342
|
_info(" " + _cy("akash mcp serve"))
|
|
4319
4343
|
_p()
|
|
@@ -4360,5 +4384,271 @@ def mcp_status(
|
|
|
4360
4384
|
print(" " + _GR + "──────────────────────────────────────────────────" + _RS)
|
|
4361
4385
|
_p()
|
|
4362
4386
|
|
|
4387
|
+
|
|
4388
|
+
@agent_app.command("stdio")
|
|
4389
|
+
def mcp_stdio(
|
|
4390
|
+
profile_id: Optional[str] = typer.Option(None, "--id", "-i", help="Local Agent Profile ID"),
|
|
4391
|
+
):
|
|
4392
|
+
"""Runs a zero-network, local-first Model Context Protocol stdio server over stdin/stdout."""
|
|
4393
|
+
import sys, json, re, subprocess, time
|
|
4394
|
+
from pathlib import Path
|
|
4395
|
+
from . import __version__
|
|
4396
|
+
|
|
4397
|
+
agents_file = Path.home() / ".akash" / "agents.json"
|
|
4398
|
+
agents = {}
|
|
4399
|
+
if agents_file.exists():
|
|
4400
|
+
try:
|
|
4401
|
+
agents = json.loads(agents_file.read_text(encoding="utf-8"))
|
|
4402
|
+
except Exception:
|
|
4403
|
+
agents = {}
|
|
4404
|
+
|
|
4405
|
+
target_pid = profile_id or (next(reversed(list(agents.keys()))) if agents else "default")
|
|
4406
|
+
profile = agents.get(target_pid, {})
|
|
4407
|
+
scopes = profile.get("scopes", "*")
|
|
4408
|
+
pname = profile.get("name", "LocalAgent")
|
|
4409
|
+
|
|
4410
|
+
tools_definitions = [
|
|
4411
|
+
{
|
|
4412
|
+
"name": "akash_exec_command",
|
|
4413
|
+
"description": "Executes shell commands in the workspace with safety policies, secret redaction, and automatic undo snapshotting.",
|
|
4414
|
+
"inputSchema": {
|
|
4415
|
+
"type": "object",
|
|
4416
|
+
"properties": {
|
|
4417
|
+
"command": {"type": "string", "description": "The shell command to execute."}
|
|
4418
|
+
},
|
|
4419
|
+
"required": ["command"]
|
|
4420
|
+
}
|
|
4421
|
+
},
|
|
4422
|
+
{
|
|
4423
|
+
"name": "akash_read_file",
|
|
4424
|
+
"description": "Reads a file from the workspace with line-range pagination directly into context.",
|
|
4425
|
+
"inputSchema": {
|
|
4426
|
+
"type": "object",
|
|
4427
|
+
"properties": {
|
|
4428
|
+
"path": {"type": "string", "description": "Path to the file to read."},
|
|
4429
|
+
"offset": {"type": "integer", "description": "Starting line number (1-indexed).", "default": 1},
|
|
4430
|
+
"limit": {"type": "integer", "description": "Maximum number of lines to return.", "default": 200}
|
|
4431
|
+
},
|
|
4432
|
+
"required": ["path"]
|
|
4433
|
+
}
|
|
4434
|
+
},
|
|
4435
|
+
{
|
|
4436
|
+
"name": "akash_write_file",
|
|
4437
|
+
"description": "Writes or edits a file in the workspace safely with auto-undo checkpoints.",
|
|
4438
|
+
"inputSchema": {
|
|
4439
|
+
"type": "object",
|
|
4440
|
+
"properties": {
|
|
4441
|
+
"path": {"type": "string", "description": "Path to the file to write."},
|
|
4442
|
+
"content": {"type": "string", "description": "The content to write into the file."},
|
|
4443
|
+
"append": {"type": "boolean", "description": "Whether to append to the file instead of overwriting.", "default": False}
|
|
4444
|
+
},
|
|
4445
|
+
"required": ["path", "content"]
|
|
4446
|
+
}
|
|
4447
|
+
},
|
|
4448
|
+
{
|
|
4449
|
+
"name": "akash_list_dir",
|
|
4450
|
+
"description": "Lists directory entries with file sizes, types, and depth control.",
|
|
4451
|
+
"inputSchema": {
|
|
4452
|
+
"type": "object",
|
|
4453
|
+
"properties": {
|
|
4454
|
+
"path": {"type": "string", "description": "Directory path to list.", "default": "."},
|
|
4455
|
+
"depth": {"type": "integer", "description": "Depth of subdirectory recursion.", "default": 1}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
},
|
|
4459
|
+
{
|
|
4460
|
+
"name": "akash_checkpoint_create",
|
|
4461
|
+
"description": "Creates an instant rollback snapshot before executing large refactors.",
|
|
4462
|
+
"inputSchema": {
|
|
4463
|
+
"type": "object",
|
|
4464
|
+
"properties": {
|
|
4465
|
+
"label": {"type": "string", "description": "Name or tag for this checkpoint.", "default": "manual_checkpoint"}
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
},
|
|
4469
|
+
{
|
|
4470
|
+
"name": "akash_undo",
|
|
4471
|
+
"description": "Reverts workspace files to the last pre-command checkpoint.",
|
|
4472
|
+
"inputSchema": {
|
|
4473
|
+
"type": "object",
|
|
4474
|
+
"properties": {}
|
|
4475
|
+
}
|
|
4476
|
+
},
|
|
4477
|
+
{
|
|
4478
|
+
"name": "akash_scope_details",
|
|
4479
|
+
"description": "Inspects active session permissions, allowed file boundaries, and security rules.",
|
|
4480
|
+
"inputSchema": {
|
|
4481
|
+
"type": "object",
|
|
4482
|
+
"properties": {}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
]
|
|
4486
|
+
|
|
4487
|
+
while True:
|
|
4488
|
+
line = sys.stdin.readline()
|
|
4489
|
+
if not line:
|
|
4490
|
+
break
|
|
4491
|
+
line = line.strip()
|
|
4492
|
+
if not line:
|
|
4493
|
+
continue
|
|
4494
|
+
try:
|
|
4495
|
+
req = json.loads(line)
|
|
4496
|
+
except Exception:
|
|
4497
|
+
continue
|
|
4498
|
+
|
|
4499
|
+
msg_id = req.get("id")
|
|
4500
|
+
method = req.get("method")
|
|
4501
|
+
params = req.get("params", {})
|
|
4502
|
+
|
|
4503
|
+
if method == "initialize":
|
|
4504
|
+
res = {
|
|
4505
|
+
"jsonrpc": "2.0",
|
|
4506
|
+
"id": msg_id,
|
|
4507
|
+
"result": {
|
|
4508
|
+
"protocolVersion": "2024-11-05",
|
|
4509
|
+
"capabilities": {"tools": {}},
|
|
4510
|
+
"serverInfo": {"name": "akash", "version": __version__}
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
sys.stdout.write(json.dumps(res) + "\n")
|
|
4514
|
+
sys.stdout.flush()
|
|
4515
|
+
elif method == "notifications/initialized":
|
|
4516
|
+
pass
|
|
4517
|
+
elif method == "ping":
|
|
4518
|
+
if msg_id is not None:
|
|
4519
|
+
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": {}}) + "\n")
|
|
4520
|
+
sys.stdout.flush()
|
|
4521
|
+
elif method == "tools/list":
|
|
4522
|
+
res = {
|
|
4523
|
+
"jsonrpc": "2.0",
|
|
4524
|
+
"id": msg_id,
|
|
4525
|
+
"result": {"tools": tools_definitions}
|
|
4526
|
+
}
|
|
4527
|
+
sys.stdout.write(json.dumps(res) + "\n")
|
|
4528
|
+
sys.stdout.flush()
|
|
4529
|
+
elif method == "tools/call":
|
|
4530
|
+
t_name = params.get("name")
|
|
4531
|
+
t_args = params.get("arguments", {})
|
|
4532
|
+
res_content = ""
|
|
4533
|
+
is_err = False
|
|
4534
|
+
|
|
4535
|
+
if t_name == "akash_exec_command":
|
|
4536
|
+
cmd = t_args.get("command", "")
|
|
4537
|
+
is_safe, sec_reason = _sanitize_agent_command(cmd, scopes=scopes)
|
|
4538
|
+
if not is_safe:
|
|
4539
|
+
res_content = f"[BLOCKED] Security Policy Block: {sec_reason}\nCommand: {cmd}"
|
|
4540
|
+
is_err = True
|
|
4541
|
+
else:
|
|
4542
|
+
if any(k in cmd for k in ["touch", "mkdir", "mv", "cp", "sed", ">", ">>", "python", "npm", "pip"]):
|
|
4543
|
+
_save_undo_checkpoint()
|
|
4544
|
+
import signal
|
|
4545
|
+
try:
|
|
4546
|
+
proc = subprocess.Popen(
|
|
4547
|
+
cmd,
|
|
4548
|
+
shell=True,
|
|
4549
|
+
stdout=subprocess.PIPE,
|
|
4550
|
+
stderr=subprocess.PIPE,
|
|
4551
|
+
stdin=subprocess.DEVNULL,
|
|
4552
|
+
text=True,
|
|
4553
|
+
start_new_session=True,
|
|
4554
|
+
errors="replace"
|
|
4555
|
+
)
|
|
4556
|
+
try:
|
|
4557
|
+
out, err = proc.communicate(timeout=25)
|
|
4558
|
+
returncode = proc.returncode
|
|
4559
|
+
raw_out = (out or "") + (err or "")
|
|
4560
|
+
except subprocess.TimeoutExpired:
|
|
4561
|
+
try:
|
|
4562
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
4563
|
+
except Exception:
|
|
4564
|
+
pass
|
|
4565
|
+
raw_out = "[BLOCKED] Command timed out after 25 seconds."
|
|
4566
|
+
returncode = 124
|
|
4567
|
+
except Exception as cmd_err:
|
|
4568
|
+
raw_out = f"[BLOCKED] Command execution error: {cmd_err}"
|
|
4569
|
+
returncode = 1
|
|
4570
|
+
bounded_out = _smart_truncate_output(raw_out)
|
|
4571
|
+
if returncode != 0:
|
|
4572
|
+
res_content = bounded_out or f"Command exited with status code {returncode}"
|
|
4573
|
+
is_err = True
|
|
4574
|
+
else:
|
|
4575
|
+
res_content = bounded_out or "(command completed successfully with no output)"
|
|
4576
|
+
elif t_name == "akash_read_file":
|
|
4577
|
+
res_content, is_err = _handle_tool_read_file(t_args, scopes)
|
|
4578
|
+
elif t_name == "akash_write_file":
|
|
4579
|
+
_save_undo_checkpoint()
|
|
4580
|
+
res_content, is_err = _handle_tool_write_file(t_args, scopes)
|
|
4581
|
+
elif t_name == "akash_list_dir":
|
|
4582
|
+
res_content, is_err = _handle_tool_list_dir(t_args, scopes)
|
|
4583
|
+
elif t_name == "akash_checkpoint_create":
|
|
4584
|
+
label = t_args.get("label", "agent_checkpoint")
|
|
4585
|
+
from .terminal import build_snapshot
|
|
4586
|
+
_, manifest, _ = build_snapshot(label=label)
|
|
4587
|
+
res_content = f"Checkpoint '{label}' created ({len(manifest.get('files', []))} files tracked)"
|
|
4588
|
+
elif t_name == "akash_undo":
|
|
4589
|
+
ok, msg_txt = _restore_undo_checkpoint()
|
|
4590
|
+
res_content = msg_txt
|
|
4591
|
+
is_err = not ok
|
|
4592
|
+
elif t_name == "akash_scope_details":
|
|
4593
|
+
res_content, is_err = _handle_tool_scope_details(scopes, pname, target_pid)
|
|
4594
|
+
else:
|
|
4595
|
+
res_content = f"Tool '{t_name}' not found."
|
|
4596
|
+
is_err = True
|
|
4597
|
+
|
|
4598
|
+
sys.stdout.write(json.dumps({
|
|
4599
|
+
"jsonrpc": "2.0",
|
|
4600
|
+
"id": msg_id,
|
|
4601
|
+
"result": {
|
|
4602
|
+
"content": [{"type": "text", "text": res_content}],
|
|
4603
|
+
"isError": is_err
|
|
4604
|
+
}
|
|
4605
|
+
}) + "\n")
|
|
4606
|
+
sys.stdout.flush()
|
|
4607
|
+
else:
|
|
4608
|
+
if msg_id is not None:
|
|
4609
|
+
sys.stdout.write(json.dumps({
|
|
4610
|
+
"jsonrpc": "2.0",
|
|
4611
|
+
"id": msg_id,
|
|
4612
|
+
"error": {"code": -32601, "message": f"Method '{method}' not found"}
|
|
4613
|
+
}) + "\n")
|
|
4614
|
+
sys.stdout.flush()
|
|
4615
|
+
|
|
4616
|
+
|
|
4617
|
+
@app.command("tunnel")
|
|
4618
|
+
@agent_app.command("tunnel")
|
|
4619
|
+
def cmd_tunnel(
|
|
4620
|
+
url: Optional[str] = typer.Option(None, "--url", "-u", help="Set custom domain or Cloudflare tunnel URL"),
|
|
4621
|
+
reset: bool = typer.Option(False, "--reset", "-r", help="Reset domain back to default akashbox.duckdns.org"),
|
|
4622
|
+
):
|
|
4623
|
+
"""Configure or view custom domain / Cloudflare tunnel for internet mode."""
|
|
4624
|
+
if reset:
|
|
4625
|
+
ConfigManager.delete("api_url")
|
|
4626
|
+
_ok(f"Reset storage and relay URL to default: {_cy(DEFAULT_STORAGE_URL)}")
|
|
4627
|
+
return
|
|
4628
|
+
|
|
4629
|
+
if url:
|
|
4630
|
+
clean = url.strip().rstrip("/")
|
|
4631
|
+
if not clean.startswith(("http://", "https://")):
|
|
4632
|
+
clean = f"https://{clean}"
|
|
4633
|
+
ConfigManager.set("api_url", clean)
|
|
4634
|
+
_ok(f"Configured active gateway domain to: {_cy(clean)}")
|
|
4635
|
+
_info("All remote MCP and API operations will now route through this domain.")
|
|
4636
|
+
return
|
|
4637
|
+
|
|
4638
|
+
cfg = ConfigManager.load()
|
|
4639
|
+
current_url = cfg.get("api_url", DEFAULT_STORAGE_URL)
|
|
4640
|
+
_p()
|
|
4641
|
+
print(f" {_C}◈{_RS} {_W}Akash Gateway Domain Configuration{_RS}")
|
|
4642
|
+
print(f" {_GR}──────────────────────────────────────────────────{_RS}")
|
|
4643
|
+
print(f" {_DM}Active Domain :{_RS} {_cy(current_url)}")
|
|
4644
|
+
print(f" {_DM}Default Relay :{_RS} {_dim(DEFAULT_STORAGE_URL)}")
|
|
4645
|
+
print(f" {_GR}──────────────────────────────────────────────────{_RS}")
|
|
4646
|
+
_p()
|
|
4647
|
+
_info("How to connect your own domain or Cloudflare Tunnel:")
|
|
4648
|
+
print(f" 1. Start Cloudflare Tunnel locally : {_cy('cloudflared tunnel --url http://127.0.0.1:8000')}")
|
|
4649
|
+
print(f" 2. Configure tunnel in akashcli : {_cy('akash tunnel --url https://<your-subdomain>.trycloudflare.com')}")
|
|
4650
|
+
print(f" 3. Reset back to Akashbox relay : {_cy('akash tunnel --reset')}")
|
|
4651
|
+
_p()
|
|
4652
|
+
|
|
4363
4653
|
if __name__ == "__main__":
|
|
4364
4654
|
app()
|
|
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
|
|
File without changes
|
|
File without changes
|