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.
Files changed (23) hide show
  1. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/PKG-INFO +1 -1
  2. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/pyproject.toml +2 -2
  3. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/commands/ai.py +109 -8
  4. nodepick_cli-0.1.3/src/nodepick_cli/commands/network.py +127 -0
  5. nodepick_cli-0.1.3/src/nodepick_cli/commands/node.py +420 -0
  6. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/exceptions.py +6 -2
  7. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/main.py +9 -1
  8. nodepick_cli-0.1.3/test_cli.py +561 -0
  9. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/uv.lock +3 -3
  10. nodepick_cli-0.1.2/src/nodepick_cli/commands/node.py +0 -235
  11. nodepick_cli-0.1.2/test_cli.py +0 -226
  12. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/.gitignore +0 -0
  13. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/CHANGELOG.md +0 -0
  14. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/README.md +0 -0
  15. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/install.sh +0 -0
  16. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/__init__.py +0 -0
  17. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/auth/__init__.py +0 -0
  18. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/auth/login.py +0 -0
  19. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/commands/__init__.py +0 -0
  20. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/commands/ssh.py +0 -0
  21. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/__init__.py +0 -0
  22. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/config.py +0 -0
  23. {nodepick_cli-0.1.2 → nodepick_cli-0.1.3}/src/nodepick_cli/core/formatters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nodepick-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Official Python CLI for nodepick.ai
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: httpx>=0.24.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nodepick-cli"
7
- version = "0.1.2"
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
- "insecure": True,
135
- "insecureSkipVerify": True,
136
- "rejectUnauthorized": False,
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")