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.
- general_augment_cli-0.1.0.dist-info/METADATA +180 -0
- general_augment_cli-0.1.0.dist-info/RECORD +42 -0
- general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
- general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
- platform_cli/__init__.py +5 -0
- platform_cli/branding.py +27 -0
- platform_cli/client.py +179 -0
- platform_cli/commands/__init__.py +1 -0
- platform_cli/commands/approvals.py +150 -0
- platform_cli/commands/auth.py +96 -0
- platform_cli/commands/billing.py +143 -0
- platform_cli/commands/channels.py +212 -0
- platform_cli/commands/deploy.py +72 -0
- platform_cli/commands/dev.py +38 -0
- platform_cli/commands/doctor.py +170 -0
- platform_cli/commands/identity.py +433 -0
- platform_cli/commands/init.py +55 -0
- platform_cli/commands/integrate.py +94 -0
- platform_cli/commands/keys.py +116 -0
- platform_cli/commands/logs.py +43 -0
- platform_cli/commands/mcp.py +258 -0
- platform_cli/commands/memory.py +316 -0
- platform_cli/commands/mock.py +30 -0
- platform_cli/commands/model_providers.py +226 -0
- platform_cli/commands/observability.py +174 -0
- platform_cli/commands/onboarding.py +72 -0
- platform_cli/commands/projects.py +302 -0
- platform_cli/commands/skills.py +116 -0
- platform_cli/commands/smoke.py +280 -0
- platform_cli/commands/status.py +49 -0
- platform_cli/commands/tools.py +179 -0
- platform_cli/commands/users.py +150 -0
- platform_cli/commands/validate.py +96 -0
- platform_cli/commands/verify.py +648 -0
- platform_cli/config.py +114 -0
- platform_cli/errors.py +103 -0
- platform_cli/local_mock.py +1392 -0
- platform_cli/main.py +130 -0
- platform_cli/openapi.py +1048 -0
- platform_cli/output.py +47 -0
- platform_cli/readiness.py +176 -0
- 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}")
|