nodepick-cli 0.1.2__tar.gz → 0.1.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.
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/PKG-INFO +1 -1
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/pyproject.toml +2 -2
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/commands/ai.py +109 -8
- nodepick_cli-0.1.3/src/nodepick_cli/commands/network.py +127 -0
- nodepick_cli-0.1.3/src/nodepick_cli/commands/node.py +420 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/exceptions.py +6 -2
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/main.py +9 -1
- nodepick_cli-0.1.3/test_cli.py +561 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/uv.lock +3 -3
- nodepick_cli-0.1.2/src/nodepick_cli/commands/node.py +0 -235
- nodepick_cli-0.1.2/test_cli.py +0 -226
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/.gitignore +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/CHANGELOG.md +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/README.md +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/install.sh +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/__init__.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/auth/__init__.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/auth/login.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/commands/__init__.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/commands/ssh.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/__init__.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/config.py +0 -0
- {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/formatters.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nodepick-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Official Python CLI for nodepick.ai"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -25,7 +25,7 @@ np = "nodepick_cli.main:app"
|
|
|
25
25
|
packages = ["src/nodepick_cli"]
|
|
26
26
|
|
|
27
27
|
[tool.uv.sources]
|
|
28
|
-
nodepick = { path = "../sdk/python" }
|
|
28
|
+
nodepick = { path = "../sdk/python", editable = true }
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
|
|
@@ -58,6 +58,57 @@ def configure_antigravity_mcp(servers: Dict[str, Any]) -> List[Path]:
|
|
|
58
58
|
return updated_files
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _delete_json_mcp_servers(file_path: Path, server_names: Optional[List[str]] = None) -> List[str]:
|
|
62
|
+
"""Safely remove MCP servers from a JSON configuration file."""
|
|
63
|
+
if not file_path.exists():
|
|
64
|
+
return []
|
|
65
|
+
try:
|
|
66
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
67
|
+
content = f.read().strip()
|
|
68
|
+
if not content:
|
|
69
|
+
return []
|
|
70
|
+
data = json.loads(content)
|
|
71
|
+
except Exception:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
if not isinstance(data, dict) or "mcpServers" not in data or not isinstance(data["mcpServers"], dict):
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
removed = []
|
|
78
|
+
if server_names:
|
|
79
|
+
for name in server_names:
|
|
80
|
+
if name in data["mcpServers"]:
|
|
81
|
+
del data["mcpServers"][name]
|
|
82
|
+
removed.append(name)
|
|
83
|
+
else:
|
|
84
|
+
removed = list(data["mcpServers"].keys())
|
|
85
|
+
data["mcpServers"] = {}
|
|
86
|
+
|
|
87
|
+
if removed:
|
|
88
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
89
|
+
json.dump(data, f, indent=2)
|
|
90
|
+
|
|
91
|
+
return removed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def delete_antigravity_mcp(server_names: Optional[List[str]] = None) -> List[str]:
|
|
95
|
+
"""Remove MCP servers for Google Antigravity across its standard configuration locations."""
|
|
96
|
+
home = Path.home()
|
|
97
|
+
target_files = [
|
|
98
|
+
home / ".gemini" / "config" / "mcp_config.json",
|
|
99
|
+
home / ".gemini" / "settings.json",
|
|
100
|
+
home / ".gemini" / "antigravity-cli" / "settings.json",
|
|
101
|
+
]
|
|
102
|
+
all_removed = set()
|
|
103
|
+
for path in target_files:
|
|
104
|
+
try:
|
|
105
|
+
removed = _delete_json_mcp_servers(path, server_names)
|
|
106
|
+
all_removed.update(removed)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
console.print(f"[dim yellow]Warning: Failed modifying {path}: {e}[/dim yellow]")
|
|
109
|
+
return list(all_removed)
|
|
110
|
+
|
|
111
|
+
|
|
61
112
|
@mcp_app.command("configure")
|
|
62
113
|
def mcp_configure(
|
|
63
114
|
agent: str = typer.Argument(
|
|
@@ -67,8 +118,19 @@ def mcp_configure(
|
|
|
67
118
|
nodes: Optional[List[str]] = typer.Argument(
|
|
68
119
|
None, help="Node IDs or display names to configure MCP for (if omitted, configures all nodes)"
|
|
69
120
|
),
|
|
121
|
+
insecure: bool = typer.Option(
|
|
122
|
+
False,
|
|
123
|
+
"--insecure",
|
|
124
|
+
help="Disable HTTPS certificate verification for MCP server connections.",
|
|
125
|
+
),
|
|
126
|
+
delete: bool = typer.Option(
|
|
127
|
+
False,
|
|
128
|
+
"--delete",
|
|
129
|
+
"-d",
|
|
130
|
+
help="Delete the MCP server configuration for the specified node(s) or agent.",
|
|
131
|
+
),
|
|
70
132
|
):
|
|
71
|
-
"""Configure MCP servers for an AI agent (e.g. antigravity) to connect to compute nodes."""
|
|
133
|
+
"""Configure or delete MCP servers for an AI agent (e.g. antigravity) to connect to compute nodes."""
|
|
72
134
|
agent_clean = agent.strip().lower()
|
|
73
135
|
if agent_clean not in ("antigravity", "agy", "gemini"):
|
|
74
136
|
console.print(
|
|
@@ -78,6 +140,42 @@ def mcp_configure(
|
|
|
78
140
|
)
|
|
79
141
|
raise typer.Exit(1)
|
|
80
142
|
|
|
143
|
+
if delete:
|
|
144
|
+
try:
|
|
145
|
+
names_to_delete = list(nodes) if nodes else None
|
|
146
|
+
if names_to_delete:
|
|
147
|
+
try:
|
|
148
|
+
client = get_client()
|
|
149
|
+
for n in list(names_to_delete):
|
|
150
|
+
try:
|
|
151
|
+
details = client.node_get_details(n)
|
|
152
|
+
for key in ("display_name", "displayName", "name", "vm_uuid", "id"):
|
|
153
|
+
val = details.get(key)
|
|
154
|
+
if val and val not in names_to_delete:
|
|
155
|
+
names_to_delete.append(val)
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
if agent_clean in ("antigravity", "agy", "gemini"):
|
|
162
|
+
removed = delete_antigravity_mcp(names_to_delete)
|
|
163
|
+
if removed:
|
|
164
|
+
for r in sorted(removed):
|
|
165
|
+
console.print(f"[bold green]✔[/bold green] Removed MCP server configuration '[bold]{r}[/bold]'")
|
|
166
|
+
console.print(
|
|
167
|
+
f"[bold green]Successfully removed {len(removed)} MCP server configuration(s) for {agent_clean}.[/bold green]"
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
if nodes:
|
|
171
|
+
console.print(f"[yellow]No matching MCP server configurations found to delete for: {', '.join(nodes)}[/yellow]")
|
|
172
|
+
else:
|
|
173
|
+
console.print(f"[yellow]No MCP server configurations found to delete for {agent_clean}.[/yellow]")
|
|
174
|
+
return
|
|
175
|
+
except Exception as e:
|
|
176
|
+
handle_error(e, "Error deleting MCP configuration")
|
|
177
|
+
return
|
|
178
|
+
|
|
81
179
|
client = get_client()
|
|
82
180
|
|
|
83
181
|
try:
|
|
@@ -131,15 +229,18 @@ def mcp_configure(
|
|
|
131
229
|
"type": "http",
|
|
132
230
|
"transport": "http",
|
|
133
231
|
"protocol": "streamable_http",
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"verify": False,
|
|
138
|
-
"tls": {
|
|
232
|
+
}
|
|
233
|
+
if insecure:
|
|
234
|
+
server_config.update({
|
|
139
235
|
"insecure": True,
|
|
140
236
|
"insecureSkipVerify": True,
|
|
141
|
-
|
|
142
|
-
|
|
237
|
+
"rejectUnauthorized": False,
|
|
238
|
+
"verify": False,
|
|
239
|
+
"tls": {
|
|
240
|
+
"insecure": True,
|
|
241
|
+
"insecureSkipVerify": True,
|
|
242
|
+
},
|
|
243
|
+
})
|
|
143
244
|
if mcp_api_key:
|
|
144
245
|
server_config["headers"] = {
|
|
145
246
|
"Authorization": f"Bearer {mcp_api_key}",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
import nodepick
|
|
6
|
+
from ..commands.node import get_client
|
|
7
|
+
from ..core.exceptions import handle_error
|
|
8
|
+
from ..core.formatters import OutputFormat, set_output_format, print_output
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(name="network", help="Manage VPC Networks.")
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _render_networks_table(networks):
|
|
15
|
+
if not networks:
|
|
16
|
+
console.print("[yellow]No VPC networks found.[/yellow]")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
table = Table("ID", "Name", "Subnet", "Region", "NAT")
|
|
20
|
+
for net in networks:
|
|
21
|
+
table.add_row(
|
|
22
|
+
str(net.get("id") or "N/A"),
|
|
23
|
+
str(net.get("name") or "N/A"),
|
|
24
|
+
str(net.get("subnet") or "N/A"),
|
|
25
|
+
str(net.get("region") or "-"),
|
|
26
|
+
"Enabled" if net.get("enableOutboundNat", True) else "Disabled",
|
|
27
|
+
)
|
|
28
|
+
console.print(table)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _render_network_details_table(network):
|
|
32
|
+
if not network:
|
|
33
|
+
console.print("[yellow]No VPC network details found.[/yellow]")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
table = Table("Field", "Value")
|
|
37
|
+
fields = [
|
|
38
|
+
("ID", network.get("id")),
|
|
39
|
+
("Name", network.get("name")),
|
|
40
|
+
("Description", network.get("description")),
|
|
41
|
+
("Subnet", network.get("subnet")),
|
|
42
|
+
("Gateway", network.get("gateway")),
|
|
43
|
+
("Region", network.get("region")),
|
|
44
|
+
("VNI", network.get("vni")),
|
|
45
|
+
("Outbound NAT", "Enabled" if network.get("enableOutboundNat", True) else "Disabled"),
|
|
46
|
+
("Created At", network.get("createdAt") or network.get("created_at")),
|
|
47
|
+
]
|
|
48
|
+
for field, value in fields:
|
|
49
|
+
if value is not None:
|
|
50
|
+
table.add_row(field, str(value))
|
|
51
|
+
console.print(table)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("list")
|
|
55
|
+
def network_list(
|
|
56
|
+
format: OutputFormat = typer.Option(
|
|
57
|
+
OutputFormat.TABLE,
|
|
58
|
+
"--format", "-f",
|
|
59
|
+
help="Output format (table or json).",
|
|
60
|
+
case_sensitive=False,
|
|
61
|
+
),
|
|
62
|
+
):
|
|
63
|
+
"""List organization VPC networks."""
|
|
64
|
+
set_output_format(format)
|
|
65
|
+
client = get_client()
|
|
66
|
+
try:
|
|
67
|
+
networks = client.vpc_list()
|
|
68
|
+
print_output(networks, table_render_func=_render_networks_table)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
handle_error(e, "Error listing VPC networks")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("create")
|
|
74
|
+
def network_create(
|
|
75
|
+
name: str = typer.Argument(..., help="Alphanumeric name for the VPC network"),
|
|
76
|
+
region: str = typer.Option(..., "--region", "-r", help="Target geographic region (e.g. us-west-1)"),
|
|
77
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="Optional description"),
|
|
78
|
+
enable_nat: bool = typer.Option(True, "--nat/--no-nat", help="Enable outbound NAT gateway"),
|
|
79
|
+
):
|
|
80
|
+
"""Create a new VPC network."""
|
|
81
|
+
client = get_client()
|
|
82
|
+
try:
|
|
83
|
+
console.print(f"[cyan]Creating VPC network '{name}'...[/cyan]")
|
|
84
|
+
network = client.vpc_create(
|
|
85
|
+
name=name,
|
|
86
|
+
region=region,
|
|
87
|
+
description=description,
|
|
88
|
+
enable_outbound_nat=enable_nat,
|
|
89
|
+
)
|
|
90
|
+
vpc_id = network.get("id")
|
|
91
|
+
console.print(f"[bold green]VPC network created successfully![/bold green] ID: {vpc_id}")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
handle_error(e, "Error creating VPC network")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("get")
|
|
97
|
+
def network_get(
|
|
98
|
+
vpc_id: str = typer.Argument(..., help="VPC Network ID or name"),
|
|
99
|
+
format: OutputFormat = typer.Option(
|
|
100
|
+
OutputFormat.TABLE,
|
|
101
|
+
"--format", "-f",
|
|
102
|
+
help="Output format (table or json).",
|
|
103
|
+
case_sensitive=False,
|
|
104
|
+
),
|
|
105
|
+
):
|
|
106
|
+
"""Get details of a specific VPC network."""
|
|
107
|
+
set_output_format(format)
|
|
108
|
+
client = get_client()
|
|
109
|
+
try:
|
|
110
|
+
network = client.vpc_get(vpc_id)
|
|
111
|
+
print_output(network, table_render_func=_render_network_details_table)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
handle_error(e, "Error getting VPC network details")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command("delete")
|
|
117
|
+
def network_delete(
|
|
118
|
+
vpc_id: str = typer.Argument(..., help="VPC Network ID or name to delete"),
|
|
119
|
+
):
|
|
120
|
+
"""Delete an organization VPC network."""
|
|
121
|
+
client = get_client()
|
|
122
|
+
try:
|
|
123
|
+
console.print(f"[cyan]Deleting VPC network {vpc_id}...[/cyan]")
|
|
124
|
+
res = client.vpc_delete(vpc_id)
|
|
125
|
+
console.print(f"[bold green]VPC network {vpc_id} deleted.[/bold green]")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
handle_error(e, "Error deleting VPC network")
|