general-augment-cli 0.1.0__py3-none-any.whl

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 (42) hide show
  1. general_augment_cli-0.1.0.dist-info/METADATA +180 -0
  2. general_augment_cli-0.1.0.dist-info/RECORD +42 -0
  3. general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
  4. general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. platform_cli/__init__.py +5 -0
  6. platform_cli/branding.py +27 -0
  7. platform_cli/client.py +179 -0
  8. platform_cli/commands/__init__.py +1 -0
  9. platform_cli/commands/approvals.py +150 -0
  10. platform_cli/commands/auth.py +96 -0
  11. platform_cli/commands/billing.py +143 -0
  12. platform_cli/commands/channels.py +212 -0
  13. platform_cli/commands/deploy.py +72 -0
  14. platform_cli/commands/dev.py +38 -0
  15. platform_cli/commands/doctor.py +170 -0
  16. platform_cli/commands/identity.py +433 -0
  17. platform_cli/commands/init.py +55 -0
  18. platform_cli/commands/integrate.py +94 -0
  19. platform_cli/commands/keys.py +116 -0
  20. platform_cli/commands/logs.py +43 -0
  21. platform_cli/commands/mcp.py +258 -0
  22. platform_cli/commands/memory.py +316 -0
  23. platform_cli/commands/mock.py +30 -0
  24. platform_cli/commands/model_providers.py +226 -0
  25. platform_cli/commands/observability.py +174 -0
  26. platform_cli/commands/onboarding.py +72 -0
  27. platform_cli/commands/projects.py +302 -0
  28. platform_cli/commands/skills.py +116 -0
  29. platform_cli/commands/smoke.py +280 -0
  30. platform_cli/commands/status.py +49 -0
  31. platform_cli/commands/tools.py +179 -0
  32. platform_cli/commands/users.py +150 -0
  33. platform_cli/commands/validate.py +96 -0
  34. platform_cli/commands/verify.py +648 -0
  35. platform_cli/config.py +114 -0
  36. platform_cli/errors.py +103 -0
  37. platform_cli/local_mock.py +1392 -0
  38. platform_cli/main.py +130 -0
  39. platform_cli/openapi.py +1048 -0
  40. platform_cli/output.py +47 -0
  41. platform_cli/readiness.py +176 -0
  42. platform_cli/runtime.py +22 -0
@@ -0,0 +1,94 @@
1
+ """OpenAPI integration scaffolding command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from platform_cli.client import encode_path_segment
11
+ from platform_cli.openapi import scaffold_from_openapi
12
+ from platform_cli.output import print_success, print_warning, table
13
+ from platform_cli.runtime import Runtime
14
+
15
+
16
+ def integrate(
17
+ ctx: typer.Context,
18
+ spec_url: Annotated[str, typer.Argument(help="OpenAPI spec URL or file path.")],
19
+ name: Annotated[str | None, typer.Option(help="Agent name.")] = None,
20
+ output_dir: Annotated[Path | None, typer.Option(help="Output directory.")] = None,
21
+ description: Annotated[
22
+ str | None,
23
+ typer.Option(help="Agent personality description."),
24
+ ] = None,
25
+ target_count: Annotated[int, typer.Option(help="Target generated tool count.")] = 15,
26
+ force: Annotated[bool, typer.Option(help="Overwrite existing generated files.")] = False,
27
+ auto_deploy: Annotated[
28
+ bool,
29
+ typer.Option(help="Deploy immediately after scaffolding."),
30
+ ] = False,
31
+ ) -> None:
32
+ """Generate a local agent scaffold from an OpenAPI spec."""
33
+ runtime: Runtime = ctx.obj
34
+ result = scaffold_from_openapi(
35
+ spec_url,
36
+ output_dir=output_dir,
37
+ name=name,
38
+ description=description,
39
+ target_count=target_count,
40
+ force=force,
41
+ )
42
+ rows: list[list[object]] = [
43
+ [
44
+ "disabled" if not tool.enabled else "enabled",
45
+ tool.tool_id,
46
+ tool.http_method,
47
+ tool.risk_level,
48
+ "yes" if tool.requires_approval else "no",
49
+ ]
50
+ for tool in result.tools
51
+ ]
52
+ table(
53
+ f"Generated {len(result.tools)} tools from {len(result.parsed_api.tools)} endpoints",
54
+ ["State", "Tool", "Method", "Risk", "Approval"],
55
+ rows,
56
+ )
57
+ print_success(f"Generated scaffold in {result.root}")
58
+ print_success(f"Coding-agent handoff written to {result.agent_prompt_path}")
59
+ if any(not tool.enabled for tool in result.tools):
60
+ print_warning("Destructive operations are disabled by default.")
61
+ if auto_deploy:
62
+ from platform_cli.commands.deploy import deploy_path
63
+
64
+ project = deploy_path(runtime, result.config_path, project_ref=None)
65
+ project_id = project.get("id")
66
+ if not project_id:
67
+ print_warning(
68
+ "Project deployed, but OpenAPI tools were not registered: missing project id."
69
+ )
70
+ return
71
+ with runtime.client() as client:
72
+ response = client.admin(
73
+ "POST",
74
+ f"/projects/{encode_path_segment(str(project_id))}/tools/from-openapi",
75
+ json={
76
+ "spec_url": _deployable_spec_source(spec_url),
77
+ "target_count": target_count,
78
+ "auto_deploy": True,
79
+ },
80
+ )
81
+ generated = response.get("generated_count", 0)
82
+ curated = response.get("curated_count", 0)
83
+ enabled = len(response.get("enabled_tool_ids") or [])
84
+ print_success(
85
+ f"Registered OpenAPI tools: {enabled} enabled, {curated} curated, {generated} generated"
86
+ )
87
+
88
+
89
+ def _deployable_spec_source(spec_url: str) -> str:
90
+ """Return a server-readable OpenAPI source for hosted registration."""
91
+ spec_path = Path(spec_url).expanduser()
92
+ if spec_path.exists() and spec_path.is_file():
93
+ return spec_path.read_text(encoding="utf-8")
94
+ return spec_url
@@ -0,0 +1,116 @@
1
+ """API key management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from platform_cli.client import encode_path_segment, resolve_project
10
+ from platform_cli.output import panel, print_success, print_warning, table
11
+ from platform_cli.runtime import Runtime
12
+
13
+ app = typer.Typer(help="Manage project-scoped API keys.")
14
+
15
+
16
+ @app.command("list")
17
+ def list_keys(ctx: typer.Context) -> None:
18
+ """List masked API keys visible to this management credential."""
19
+
20
+ runtime: Runtime = ctx.obj
21
+ with runtime.client() as client:
22
+ payload = client.admin("GET", "/keys")
23
+ items = payload.get("items", []) if isinstance(payload, dict) else []
24
+ rows = [
25
+ [
26
+ item.get("name", ""),
27
+ item.get("masked_key", ""),
28
+ item.get("project_id", ""),
29
+ ",".join(item.get("scopes", [])),
30
+ item.get("expires_at", "") or "",
31
+ item.get("id", ""),
32
+ ]
33
+ for item in items
34
+ if isinstance(item, dict)
35
+ ]
36
+ table("API Keys", ["Name", "Key", "Project", "Scopes", "Expires", "ID"], rows)
37
+
38
+
39
+ @app.command("create")
40
+ def create_key(
41
+ ctx: typer.Context,
42
+ name: str = typer.Option(..., help="Display name, for example Production backend."),
43
+ project: str | None = typer.Option(None, help="Project id, slug, or name."),
44
+ scope: Annotated[
45
+ list[str] | None,
46
+ typer.Option("--scope", help="Scope to grant; repeatable."),
47
+ ] = None,
48
+ expires_at: str | None = typer.Option(None, help="Optional ISO-8601 expiration timestamp."),
49
+ ) -> None:
50
+ """Create an API key and print the raw secret once."""
51
+
52
+ runtime: Runtime = ctx.obj
53
+ payload: dict[str, object] = {
54
+ "name": name,
55
+ "scopes": scope or ["admin"],
56
+ }
57
+ with runtime.client() as client:
58
+ if project:
59
+ project_payload = resolve_project(client, project)
60
+ payload["project_id"] = str(project_payload["id"])
61
+ if expires_at is not None:
62
+ payload["expires_at"] = expires_at
63
+ response = client.admin("POST", "/keys", json=payload)
64
+ print_success(
65
+ f"Created API key {response.get('name', name)} ({response.get('id', 'unknown')})."
66
+ )
67
+ print_warning("The raw API key is shown once. Store it in your backend secret manager.")
68
+ panel(
69
+ "API Key",
70
+ f"api_key: {response.get('api_key', '')}\n"
71
+ f"masked_key: {response.get('masked_key', '')}\n"
72
+ f"project_id: {response.get('project_id', payload.get('project_id', ''))}\n"
73
+ f"scopes: {','.join(response.get('scopes', []))}",
74
+ )
75
+
76
+
77
+ @app.command("update")
78
+ def update_key(
79
+ ctx: typer.Context,
80
+ key_id: str = typer.Argument(..., help="API key id."),
81
+ name: str | None = typer.Option(None, help="New display name."),
82
+ scope: Annotated[
83
+ list[str] | None,
84
+ typer.Option("--scope", help="Replacement scope; repeatable."),
85
+ ] = None,
86
+ expires_at: str | None = typer.Option(None, help="Replacement ISO-8601 expiration timestamp."),
87
+ clear_expiration: bool = typer.Option(False, help="Clear the key expiration."),
88
+ ) -> None:
89
+ """Update API key metadata without showing the raw secret."""
90
+
91
+ runtime: Runtime = ctx.obj
92
+ payload: dict[str, object | None] = {}
93
+ if name is not None:
94
+ payload["name"] = name
95
+ if scope is not None:
96
+ payload["scopes"] = scope
97
+ if clear_expiration:
98
+ payload["expires_at"] = None
99
+ elif expires_at is not None:
100
+ payload["expires_at"] = expires_at
101
+ with runtime.client() as client:
102
+ response = client.admin("PATCH", f"/keys/{encode_path_segment(key_id)}", json=payload)
103
+ print_success(f"Updated API key {response.get('name', key_id)}.")
104
+
105
+
106
+ @app.command("revoke")
107
+ def revoke_key(
108
+ ctx: typer.Context,
109
+ key_id: str = typer.Argument(..., help="API key id."),
110
+ ) -> None:
111
+ """Revoke an API key."""
112
+
113
+ runtime: Runtime = ctx.obj
114
+ with runtime.client() as client:
115
+ response = client.admin("DELETE", f"/keys/{encode_path_segment(key_id)}")
116
+ print_success(f"Revoked API key {response.get('id', key_id)}.")
@@ -0,0 +1,43 @@
1
+ """Project log streaming command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ import typer
8
+
9
+ from platform_cli.client import encode_path_segment, resolve_project
10
+ from platform_cli.output import table
11
+ from platform_cli.runtime import Runtime
12
+
13
+
14
+ def logs(
15
+ ctx: typer.Context,
16
+ project: str = typer.Option(..., help="Project id, slug, or name."),
17
+ follow: bool = typer.Option(False, "--follow", help="Poll continuously."),
18
+ limit: int = typer.Option(25, min=1, max=200, help="Log row limit."),
19
+ ) -> None:
20
+ """Show recent project logs."""
21
+ runtime: Runtime = ctx.obj
22
+ with runtime.client() as client:
23
+ project_payload = resolve_project(client, project)
24
+ while True:
25
+ payload = client.admin(
26
+ "GET",
27
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/logs",
28
+ params={"limit": limit},
29
+ )
30
+ items = payload.get("items", []) if isinstance(payload, dict) else []
31
+ rows = [
32
+ [
33
+ item.get("created_at", ""),
34
+ item.get("role", ""),
35
+ str(item.get("content", ""))[:80],
36
+ ]
37
+ for item in items
38
+ if isinstance(item, dict)
39
+ ]
40
+ table("Logs", ["Time", "Role", "Content"], rows)
41
+ if not follow:
42
+ return
43
+ time.sleep(2)
@@ -0,0 +1,258 @@
1
+ """MCP server management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any
6
+
7
+ import typer
8
+
9
+ from platform_cli.client import encode_path_segment, resolve_project
10
+ from platform_cli.output import print_json, print_success, table
11
+ from platform_cli.runtime import Runtime
12
+
13
+ app = typer.Typer(help="Manage tenant MCP servers.")
14
+
15
+
16
+ @app.command("list")
17
+ def list_mcp_servers(
18
+ ctx: typer.Context,
19
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
20
+ json_output: Annotated[
21
+ bool,
22
+ typer.Option("--json", help="Print machine-readable JSON."),
23
+ ] = False,
24
+ ) -> None:
25
+ """List MCP servers configured for one project."""
26
+ runtime: Runtime = ctx.obj
27
+ with runtime.client() as client:
28
+ project_payload = resolve_project(client, project)
29
+ payload = client.admin(
30
+ "GET",
31
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers",
32
+ )
33
+ if json_output:
34
+ print_json(payload)
35
+ return
36
+ items = payload.get("items", []) if isinstance(payload, dict) else []
37
+ rows = [_server_row(item) for item in items if isinstance(item, dict)]
38
+ table("MCP servers", ["Name", "Transport", "Enabled", "Endpoint", "Tools"], rows)
39
+
40
+
41
+ @app.command("add")
42
+ def add_mcp_server(
43
+ ctx: typer.Context,
44
+ name: Annotated[str, typer.Argument(help="MCP server name.")],
45
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
46
+ url: Annotated[str | None, typer.Option(help="HTTP MCP endpoint URL.")] = None,
47
+ command: Annotated[str | None, typer.Option(help="Stdio MCP command.")] = None,
48
+ arg: Annotated[
49
+ list[str] | None,
50
+ typer.Option("--arg", help="Stdio command argument. Repeatable."),
51
+ ] = None,
52
+ header: Annotated[
53
+ list[str] | None,
54
+ typer.Option("--header", help="HTTP header as key=value. Repeatable."),
55
+ ] = None,
56
+ env: Annotated[
57
+ list[str] | None,
58
+ typer.Option("--env", help="Stdio environment value as key=value. Repeatable."),
59
+ ] = None,
60
+ include_tool: Annotated[
61
+ list[str] | None,
62
+ typer.Option("--include-tool", help="MCP tool name to expose. Repeatable."),
63
+ ] = None,
64
+ exclude_tool: Annotated[
65
+ list[str] | None,
66
+ typer.Option("--exclude-tool", help="MCP tool name to hide. Repeatable."),
67
+ ] = None,
68
+ timeout: Annotated[int | None, typer.Option(min=1, help="Runtime timeout seconds.")] = None,
69
+ connect_timeout: Annotated[
70
+ int | None,
71
+ typer.Option("--connect-timeout", min=1, help="Connection timeout seconds."),
72
+ ] = None,
73
+ enabled: Annotated[
74
+ bool,
75
+ typer.Option("--enabled/--disabled", help="Whether Hermes may use this server."),
76
+ ] = True,
77
+ json_output: Annotated[
78
+ bool,
79
+ typer.Option("--json", help="Print machine-readable JSON."),
80
+ ] = False,
81
+ ) -> None:
82
+ """Add one HTTP or stdio MCP server to a project."""
83
+ if bool(url) == bool(command):
84
+ raise typer.BadParameter("Provide exactly one transport: --url or --command.")
85
+ payload: dict[str, Any] = {"name": name, "enabled": enabled}
86
+ if url:
87
+ payload["url"] = url
88
+ if command:
89
+ payload["command"] = command
90
+ if arg:
91
+ payload["args"] = list(arg)
92
+ headers = _key_value_pairs(header or [], option="--header")
93
+ if headers:
94
+ payload["headers"] = headers
95
+ env_values = _key_value_pairs(env or [], option="--env")
96
+ if env_values:
97
+ payload["env"] = env_values
98
+ tools = _tools_filter(include_tool or [], exclude_tool or [])
99
+ if tools:
100
+ payload["tools"] = tools
101
+ if timeout is not None:
102
+ payload["timeout"] = timeout
103
+ if connect_timeout is not None:
104
+ payload["connect_timeout"] = connect_timeout
105
+ runtime: Runtime = ctx.obj
106
+ with runtime.client() as client:
107
+ project_payload = resolve_project(client, project)
108
+ response = client.admin(
109
+ "POST",
110
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers",
111
+ json=payload,
112
+ )
113
+ if json_output:
114
+ print_json(response)
115
+ return
116
+ table(
117
+ "Added MCP server",
118
+ ["Field", "Value"],
119
+ [
120
+ ["Project", project],
121
+ ["Name", _value(response, "name") or name],
122
+ ["Transport", _transport(response if isinstance(response, dict) else payload)],
123
+ ["Enabled", _value(response, "enabled")],
124
+ ["Tools", _tools_label(response if isinstance(response, dict) else payload)],
125
+ ],
126
+ )
127
+
128
+
129
+ @app.command("test")
130
+ def test_mcp_server(
131
+ ctx: typer.Context,
132
+ name: Annotated[str, typer.Argument(help="MCP server name.")],
133
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
134
+ json_output: Annotated[
135
+ bool,
136
+ typer.Option("--json", help="Print machine-readable JSON."),
137
+ ] = False,
138
+ ) -> None:
139
+ """Test one configured MCP server."""
140
+ runtime: Runtime = ctx.obj
141
+ with runtime.client() as client:
142
+ project_payload = resolve_project(client, project)
143
+ response = client.admin(
144
+ "POST",
145
+ (
146
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers/"
147
+ f"{encode_path_segment(name)}/test"
148
+ ),
149
+ )
150
+ if json_output:
151
+ print_json(response)
152
+ return
153
+ table(
154
+ "MCP server test",
155
+ ["Field", "Value"],
156
+ [
157
+ ["Name", _value(response, "name")],
158
+ ["OK", _value(response, "ok")],
159
+ ["Transport", _value(response, "transport")],
160
+ ["Detail", _value(response, "detail")],
161
+ ],
162
+ )
163
+
164
+
165
+ @app.command("delete")
166
+ def delete_mcp_server(
167
+ ctx: typer.Context,
168
+ name: Annotated[str, typer.Argument(help="MCP server name.")],
169
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
170
+ json_output: Annotated[
171
+ bool,
172
+ typer.Option("--json", help="Print machine-readable JSON."),
173
+ ] = False,
174
+ ) -> None:
175
+ """Delete one configured MCP server."""
176
+ runtime: Runtime = ctx.obj
177
+ with runtime.client() as client:
178
+ project_payload = resolve_project(client, project)
179
+ response = client.admin(
180
+ "DELETE",
181
+ (
182
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/mcp-servers/"
183
+ f"{encode_path_segment(name)}"
184
+ ),
185
+ )
186
+ if json_output:
187
+ print_json(response)
188
+ return
189
+ deleted_name = response.get("name", name) if isinstance(response, dict) else name
190
+ print_success(f"Deleted MCP server {deleted_name}.")
191
+
192
+
193
+ def _key_value_pairs(values: list[str], *, option: str) -> dict[str, str]:
194
+ """Parse repeated key=value CLI flags."""
195
+ parsed: dict[str, str] = {}
196
+ for item in values:
197
+ key, separator, value = item.partition("=")
198
+ if not separator or not key.strip():
199
+ raise typer.BadParameter(f"{option} values must use key=value.")
200
+ parsed[key.strip()] = value
201
+ return parsed
202
+
203
+
204
+ def _tools_filter(include: list[str], exclude: list[str]) -> dict[str, list[str]]:
205
+ """Return MCP tool include/exclude filters."""
206
+ tools: dict[str, list[str]] = {}
207
+ if include:
208
+ tools["include"] = include
209
+ if exclude:
210
+ tools["exclude"] = exclude
211
+ return tools
212
+
213
+
214
+ def _server_row(server: dict[str, Any]) -> list[object]:
215
+ """Return a table row for one MCP server."""
216
+ return [
217
+ server.get("name", ""),
218
+ _transport(server),
219
+ "yes" if server.get("enabled", True) else "no",
220
+ server.get("url") or server.get("command") or "",
221
+ _tools_label(server),
222
+ ]
223
+
224
+
225
+ def _transport(server: dict[str, Any]) -> str:
226
+ """Return MCP transport label."""
227
+ if server.get("url"):
228
+ return "http"
229
+ if server.get("command"):
230
+ return "stdio"
231
+ return "unknown"
232
+
233
+
234
+ def _tools_label(server: dict[str, Any]) -> str:
235
+ """Return a compact tool filter label."""
236
+ tools = server.get("tools")
237
+ if not isinstance(tools, dict):
238
+ return "all"
239
+ include = _string_list(tools.get("include"))
240
+ exclude = _string_list(tools.get("exclude"))
241
+ labels: list[str] = []
242
+ if include:
243
+ labels.append("include: " + ", ".join(include))
244
+ if exclude:
245
+ labels.append("exclude: " + ", ".join(exclude))
246
+ return "; ".join(labels) if labels else "all"
247
+
248
+
249
+ def _string_list(value: object) -> list[str]:
250
+ """Return a string list from JSON."""
251
+ return [str(item) for item in value] if isinstance(value, list) else []
252
+
253
+
254
+ def _value(payload: object, key: str) -> object:
255
+ """Safely read a value from a response mapping."""
256
+ if isinstance(payload, dict):
257
+ return payload.get(key, "")
258
+ return ""