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,96 @@
1
+ """Authentication commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ import typer
8
+ from rich.prompt import Prompt
9
+
10
+ from platform_cli.config import clear_config, save_config
11
+ from platform_cli.output import panel, print_success
12
+ from platform_cli.runtime import Runtime
13
+
14
+ app = typer.Typer(help="Authenticate the CLI.")
15
+
16
+
17
+ @app.command("login")
18
+ def login(
19
+ ctx: typer.Context,
20
+ api_key: str | None = typer.Option(None, help="Admin API key."),
21
+ base_url: str | None = typer.Option(None, help="Platform API base URL."),
22
+ skip_verify: bool = typer.Option(
23
+ False,
24
+ "--skip-verify",
25
+ help="Store the key without calling the platform API.",
26
+ ),
27
+ ) -> None:
28
+ """Store an API key locally after verifying it can reach the API."""
29
+ runtime = _runtime(ctx)
30
+ key = api_key or Prompt.ask("API key", password=True)
31
+ next_config = runtime.config.model_copy(
32
+ update={
33
+ "api_key": key,
34
+ "base_url": (base_url or runtime.config.base_url).rstrip("/"),
35
+ }
36
+ )
37
+ identity: dict[str, object] | None = None
38
+ if not skip_verify:
39
+ verify_runtime = Runtime(
40
+ config=next_config,
41
+ config_path=runtime.config_path,
42
+ loaded_config_path=runtime.loaded_config_path,
43
+ )
44
+ with verify_runtime.client() as client:
45
+ identity = client.admin("GET", "/me")
46
+ path = save_config(next_config, runtime.config_path)
47
+ print_success(f"Authenticated. Config saved to {path}")
48
+ if identity is not None:
49
+ print_success(
50
+ f"Verified API access as {identity.get('auth_method', 'api_key')!s}; "
51
+ f"projects: {_project_scope(identity)}"
52
+ )
53
+
54
+
55
+ @app.command("logout")
56
+ def logout(ctx: typer.Context) -> None:
57
+ """Remove local authentication."""
58
+ runtime = _runtime(ctx)
59
+ clear_config(runtime.config_path)
60
+ if runtime.loaded_config_path != runtime.config_path:
61
+ clear_config(runtime.loaded_config_path)
62
+ print_success("Logged out.")
63
+
64
+
65
+ @app.command("whoami")
66
+ def whoami(ctx: typer.Context) -> None:
67
+ """Show the current API identity."""
68
+ runtime = _runtime(ctx)
69
+ if not runtime.config.api_key:
70
+ panel("Not authenticated", "Run genaug auth login to configure an API key.")
71
+ return
72
+ with runtime.client() as client:
73
+ payload = client.admin("GET", "/me")
74
+ panel(
75
+ "Current identity",
76
+ f"Base URL: {runtime.config.base_url}\n"
77
+ f"Auth method: {payload.get('auth_method', 'unknown')}\n"
78
+ f"Project IDs: {_project_scope(payload)}",
79
+ )
80
+
81
+
82
+ def _runtime(ctx: typer.Context) -> Runtime:
83
+ """Return the current runtime object."""
84
+ return cast(Runtime, ctx.obj)
85
+
86
+
87
+ def _project_scope(identity: dict[str, object]) -> str:
88
+ """Return a display string for project scope from /me."""
89
+ raw_project_ids = identity.get("project_ids")
90
+ if isinstance(raw_project_ids, list) and raw_project_ids:
91
+ project_ids = raw_project_ids
92
+ elif identity.get("project_id"):
93
+ project_ids = [identity["project_id"]]
94
+ else:
95
+ project_ids = []
96
+ return ", ".join(str(project_id) for project_id in project_ids) or "global"
@@ -0,0 +1,143 @@
1
+ """Billing lifecycle 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 print_json, print_success, print_warning, table
11
+ from platform_cli.runtime import Runtime
12
+
13
+ app = typer.Typer(help="Manage hosted billing lifecycle actions.")
14
+
15
+
16
+ @app.command("checkout")
17
+ def create_checkout_session(
18
+ ctx: typer.Context,
19
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
20
+ tier: Annotated[str, typer.Option(help="Paid target tier: pro or team.")],
21
+ json_output: Annotated[
22
+ bool,
23
+ typer.Option("--json", help="Print machine-readable JSON."),
24
+ ] = False,
25
+ ) -> None:
26
+ """Create a hosted Stripe Checkout session URL for a paid tier."""
27
+ target_tier = _target_tier(tier)
28
+ runtime: Runtime = ctx.obj
29
+ with runtime.client() as client:
30
+ project_payload = resolve_project(client, project)
31
+ response = client.admin(
32
+ "POST",
33
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/billing/checkout-session",
34
+ json={"target_tier": target_tier},
35
+ )
36
+ if json_output:
37
+ print_json(response)
38
+ return
39
+ url = _value(response, "url")
40
+ print_success(
41
+ f"Created {target_tier} checkout session for {project_payload.get('slug', project)}."
42
+ )
43
+ table(
44
+ "Billing checkout",
45
+ ["Field", "Value"],
46
+ [
47
+ ["Target tier", target_tier],
48
+ ["URL", url],
49
+ ["Next step", "Open the hosted URL and confirm the Stripe webhook event syncs."],
50
+ ],
51
+ )
52
+
53
+
54
+ @app.command("portal")
55
+ def create_portal_session(
56
+ ctx: typer.Context,
57
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
58
+ json_output: Annotated[
59
+ bool,
60
+ typer.Option("--json", help="Print machine-readable JSON."),
61
+ ] = False,
62
+ ) -> None:
63
+ """Create a hosted Stripe Customer Portal session URL."""
64
+ runtime: Runtime = ctx.obj
65
+ with runtime.client() as client:
66
+ project_payload = resolve_project(client, project)
67
+ response = client.admin(
68
+ "POST",
69
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/billing/portal-session",
70
+ )
71
+ if json_output:
72
+ print_json(response)
73
+ return
74
+ url = _value(response, "url")
75
+ print_success(f"Created customer portal session for {project_payload.get('slug', project)}.")
76
+ table(
77
+ "Billing portal",
78
+ ["Field", "Value"],
79
+ [
80
+ ["URL", url],
81
+ ["Next step", "Open the hosted URL for card, invoice, cancellation, or plan actions."],
82
+ ],
83
+ )
84
+
85
+
86
+ @app.command("events")
87
+ def list_billing_events(
88
+ ctx: typer.Context,
89
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
90
+ json_output: Annotated[
91
+ bool,
92
+ typer.Option("--json", help="Print machine-readable JSON."),
93
+ ] = False,
94
+ ) -> None:
95
+ """List recent Stripe billing lifecycle events stored by General Augment."""
96
+ runtime: Runtime = ctx.obj
97
+ with runtime.client() as client:
98
+ project_payload = resolve_project(client, project)
99
+ response = client.admin(
100
+ "GET",
101
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/billing/events",
102
+ )
103
+ if json_output:
104
+ print_json(response)
105
+ return
106
+ items = response.get("items", []) if isinstance(response, dict) else []
107
+ rows = [_event_row(item) for item in items if isinstance(item, dict)]
108
+ if not rows:
109
+ print_warning("No billing events are stored for this project yet.")
110
+ table(
111
+ f"Billing events for {project_payload.get('slug', project)}",
112
+ ["Event", "Status", "Tier", "Invoice", "Processed at"],
113
+ rows,
114
+ )
115
+
116
+
117
+ def _target_tier(value: str) -> str:
118
+ """Normalize and validate a checkout target tier."""
119
+
120
+ target = value.casefold()
121
+ if target not in {"pro", "team"}:
122
+ raise typer.BadParameter("Paid target tier must be 'pro' or 'team'.")
123
+ return target
124
+
125
+
126
+ def _event_row(item: dict[str, object]) -> list[object]:
127
+ """Return one billing event table row."""
128
+
129
+ return [
130
+ item.get("event_type", ""),
131
+ item.get("status", "") or "",
132
+ item.get("target_pricing_tier", "") or "",
133
+ item.get("stripe_invoice_id", "") or "",
134
+ item.get("processed_at", "") or item.get("created_at", "") or "",
135
+ ]
136
+
137
+
138
+ def _value(payload: object, key: str) -> object:
139
+ """Safely read one response value."""
140
+
141
+ if isinstance(payload, dict):
142
+ return payload.get(key, "")
143
+ return ""
@@ -0,0 +1,212 @@
1
+ """Channel management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from platform_cli.client import encode_path_segment, resolve_project
8
+ from platform_cli.output import panel, print_json, print_success, table
9
+ from platform_cli.runtime import Runtime
10
+
11
+ app = typer.Typer(help="Manage channels.")
12
+
13
+
14
+ @app.command("status")
15
+ def channel_status(
16
+ ctx: typer.Context,
17
+ project: str = typer.Option(..., help="Project id, slug, or name."),
18
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
19
+ ) -> None:
20
+ """Show channel status."""
21
+ runtime: Runtime = ctx.obj
22
+ with runtime.client() as client:
23
+ project_payload = resolve_project(client, project)
24
+ telegram = client.admin(
25
+ "GET",
26
+ "/channels/telegram/status",
27
+ params={"project_id": project_payload["id"]},
28
+ )
29
+ if json_output:
30
+ print_json(
31
+ {
32
+ "project": project_payload,
33
+ "channels": {
34
+ "whatsapp": {
35
+ "connected": bool(project_payload.get("whatsapp_phone_number_id")),
36
+ "phone_number_id": project_payload.get("whatsapp_phone_number_id"),
37
+ },
38
+ "sms": {
39
+ "connected": bool(project_payload.get("twilio_phone_number")),
40
+ "twilio_phone_number": project_payload.get("twilio_phone_number"),
41
+ },
42
+ "telegram": telegram,
43
+ },
44
+ }
45
+ )
46
+ return
47
+ rows: list[list[object]] = [
48
+ ["WhatsApp", "connected" if project_payload.get("whatsapp_phone_number_id") else "open"],
49
+ ["SMS", "connected" if project_payload.get("twilio_phone_number") else "open"],
50
+ ["Telegram", "connected" if telegram.get("connected") else "open"],
51
+ ]
52
+ table("Channels", ["Channel", "Status"], rows)
53
+ if telegram.get("bot_username"):
54
+ panel(
55
+ "Telegram",
56
+ f"Bot: @{telegram['bot_username']}\n"
57
+ f"Last message: {telegram.get('last_message_at') or 'never'}\n"
58
+ f"24h messages: {telegram.get('message_count_24h', 0)}",
59
+ )
60
+
61
+
62
+ @app.command("connect")
63
+ def channel_connect(
64
+ ctx: typer.Context,
65
+ project: str = typer.Option(..., help="Project id, slug, or name."),
66
+ channel: str = typer.Option("telegram", help="Channel to connect."),
67
+ bot_token: str | None = typer.Option(None, help="Telegram bot token."),
68
+ phone_number_id: str | None = typer.Option(
69
+ None,
70
+ "--phone-number-id",
71
+ help="WhatsApp Business phone number id.",
72
+ ),
73
+ twilio_number: str | None = typer.Option(
74
+ None,
75
+ "--twilio-number",
76
+ help="Twilio SMS sender number.",
77
+ ),
78
+ webhook_base_url: str | None = typer.Option(None, help="Public API base URL."),
79
+ ) -> None:
80
+ """Connect a provider channel."""
81
+ normalized_channel = _normalize_channel(channel)
82
+ runtime: Runtime = ctx.obj
83
+ with runtime.client() as client:
84
+ project_payload = resolve_project(client, project)
85
+ if normalized_channel == "telegram":
86
+ if not bot_token:
87
+ bot_token = typer.prompt("Telegram bot token", hide_input=True)
88
+ response = client.admin(
89
+ "POST",
90
+ "/channels/telegram/connect",
91
+ json={
92
+ "project_id": str(project_payload["id"]),
93
+ "bot_token": bot_token,
94
+ "webhook_base_url": webhook_base_url or runtime.config.base_url,
95
+ },
96
+ )
97
+ print_success(f"Telegram connected: @{response.get('bot_username', 'bot')}")
98
+ return
99
+ if normalized_channel == "whatsapp":
100
+ value = _required_channel_value(
101
+ phone_number_id or typer.prompt("WhatsApp Business phone number id"),
102
+ "--phone-number-id",
103
+ )
104
+ client.admin(
105
+ "PATCH",
106
+ f"/projects/{encode_path_segment(str(project_payload['id']))}",
107
+ json={"whatsapp_phone_number_id": value},
108
+ )
109
+ print_success("WhatsApp sender configured.")
110
+ return
111
+ value = _required_channel_value(
112
+ twilio_number or typer.prompt("Twilio SMS sender number"),
113
+ "--twilio-number",
114
+ )
115
+ client.admin(
116
+ "PATCH",
117
+ f"/projects/{encode_path_segment(str(project_payload['id']))}",
118
+ json={"twilio_phone_number": value},
119
+ )
120
+ print_success("SMS sender configured.")
121
+
122
+
123
+ @app.command("disconnect")
124
+ def channel_disconnect(
125
+ ctx: typer.Context,
126
+ project: str = typer.Option(..., help="Project id, slug, or name."),
127
+ channel: str = typer.Option("telegram", help="Channel to disconnect."),
128
+ yes: bool = typer.Option(False, "--yes", help="Confirm disconnecting this channel."),
129
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
130
+ ) -> None:
131
+ """Disconnect a provider channel."""
132
+ normalized_channel = _normalize_channel(channel)
133
+ label = _channel_label(normalized_channel)
134
+ if not yes and not typer.confirm(f"Disconnect {label} for project {project}?"):
135
+ raise typer.Exit(1)
136
+ runtime: Runtime = ctx.obj
137
+ with runtime.client() as client:
138
+ project_payload = resolve_project(client, project)
139
+ if normalized_channel == "telegram":
140
+ response = client.admin(
141
+ "POST",
142
+ "/channels/telegram/disconnect",
143
+ json={"project_id": str(project_payload["id"])},
144
+ )
145
+ else:
146
+ field = (
147
+ "whatsapp_phone_number_id"
148
+ if normalized_channel == "whatsapp"
149
+ else "twilio_phone_number"
150
+ )
151
+ response = client.admin(
152
+ "PATCH",
153
+ f"/projects/{encode_path_segment(str(project_payload['id']))}",
154
+ json={field: None},
155
+ )
156
+ if json_output:
157
+ print_json(response)
158
+ return
159
+ print_success(f"{label} disconnected.")
160
+
161
+
162
+ @app.command("test")
163
+ def channel_test(
164
+ ctx: typer.Context,
165
+ project: str = typer.Option(..., help="Project id, slug, or name."),
166
+ channel: str = typer.Option("telegram", help="Channel to test."),
167
+ chat_id: str | None = typer.Option(None, help="Telegram chat id for a test message."),
168
+ message: str = typer.Option("Hello from your General Augment agent.", help="Test message."),
169
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
170
+ ) -> None:
171
+ """Send a provider test message."""
172
+ if channel != "telegram":
173
+ raise typer.BadParameter("Only telegram guided test is supported by this command.")
174
+ if not chat_id:
175
+ chat_id = typer.prompt("Telegram chat id")
176
+ runtime: Runtime = ctx.obj
177
+ with runtime.client() as client:
178
+ project_payload = resolve_project(client, project)
179
+ response = client.admin(
180
+ "POST",
181
+ "/channels/telegram/test",
182
+ json={
183
+ "project_id": str(project_payload["id"]),
184
+ "chat_id": chat_id,
185
+ "message": message,
186
+ },
187
+ )
188
+ if json_output:
189
+ print_json(response)
190
+ return
191
+ print_success("Telegram test message sent.")
192
+
193
+
194
+ def _normalize_channel(channel: str) -> str:
195
+ """Return a supported channel id."""
196
+ normalized = channel.strip().casefold()
197
+ if normalized not in {"telegram", "whatsapp", "sms"}:
198
+ raise typer.BadParameter("--channel must be one of: telegram, whatsapp, sms")
199
+ return normalized
200
+
201
+
202
+ def _channel_label(channel: str) -> str:
203
+ """Return a display label for a channel id."""
204
+ return {"telegram": "Telegram", "whatsapp": "WhatsApp", "sms": "SMS"}[channel]
205
+
206
+
207
+ def _required_channel_value(value: str, option_name: str) -> str:
208
+ """Return a non-empty channel configuration value."""
209
+ normalized = value.strip()
210
+ if not normalized:
211
+ raise typer.BadParameter(f"{option_name} is required for this channel")
212
+ return normalized
@@ -0,0 +1,72 @@
1
+ """Deploy local agent configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated, Any, cast
7
+
8
+ import typer
9
+
10
+ from platform_cli.client import encode_path_segment, resolve_project
11
+ from platform_cli.errors import CLIError
12
+ from platform_cli.openapi import load_deploy_payload, project_name_from_config
13
+ from platform_cli.output import panel, print_success
14
+ from platform_cli.runtime import Runtime
15
+
16
+
17
+ def deploy(
18
+ ctx: typer.Context,
19
+ config_path: Annotated[
20
+ Path,
21
+ typer.Argument(
22
+ exists=True,
23
+ dir_okay=False,
24
+ help="genaug-agent.yaml manifest to deploy.",
25
+ ),
26
+ ],
27
+ project: Annotated[
28
+ str | None,
29
+ typer.Option("--project", help="Project id, slug, or name."),
30
+ ] = None,
31
+ ) -> None:
32
+ """Validate and upload a local agent manifest."""
33
+ runtime: Runtime = ctx.obj
34
+ deploy_path(runtime, config_path, project_ref=project)
35
+
36
+
37
+ def deploy_path(runtime: Runtime, config_path: Path, project_ref: str | None) -> dict[str, Any]:
38
+ """Deploy a local config path."""
39
+ payload = load_deploy_payload(config_path)
40
+ local_name = project_ref or project_name_from_config(config_path)
41
+ with runtime.client() as client:
42
+ try:
43
+ existing = resolve_project(client, local_name)
44
+ except CLIError as exc:
45
+ if "Project not found" not in exc.message:
46
+ raise
47
+ existing = None
48
+ if existing:
49
+ response = cast(
50
+ dict[str, Any],
51
+ client.admin(
52
+ "PUT",
53
+ f"/projects/{encode_path_segment(str(existing['id']))}/config",
54
+ json=payload,
55
+ ),
56
+ )
57
+ action = "updated"
58
+ else:
59
+ response = cast(
60
+ dict[str, Any],
61
+ client.admin("POST", "/projects/from-config", json=payload),
62
+ )
63
+ action = "created"
64
+ name = response.get("name") or response.get("slug") or local_name
65
+ print_success(f"Project {action}: {name}")
66
+ panel(
67
+ "Webhook URLs",
68
+ f"WhatsApp: {runtime.config.base_url}/api/v1/webhooks/whatsapp\n"
69
+ f"Telegram: {runtime.config.base_url}/api/v1/webhooks/telegram\n"
70
+ f"SMS: {runtime.config.base_url}/api/v1/webhooks/sms",
71
+ )
72
+ return response
@@ -0,0 +1,38 @@
1
+ """Local development command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ import yaml
10
+
11
+ from platform_cli.output import panel
12
+
13
+
14
+ def dev(
15
+ config_path: Annotated[
16
+ Path,
17
+ typer.Argument(
18
+ exists=True,
19
+ dir_okay=False,
20
+ help="genaug-agent.yaml manifest to run locally.",
21
+ ),
22
+ ],
23
+ message: Annotated[
24
+ str | None,
25
+ typer.Option(help="Run one message and exit."),
26
+ ] = None,
27
+ ) -> None:
28
+ """Run a local mock REPL for config and personality iteration."""
29
+ payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
30
+ metadata = payload.get("metadata", {}) if isinstance(payload, dict) else {}
31
+ name = metadata.get("display_name") or metadata.get("name") or "Agent"
32
+ if message:
33
+ panel("Local dev response", f"{name} would respond to: {message}")
34
+ return
35
+ panel("Local dev", f"Loaded {name}. Type Ctrl+C to exit.")
36
+ while True:
37
+ user_message = typer.prompt("you")
38
+ panel("Local dev response", f"{name} would respond to: {user_message}")