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,170 @@
|
|
|
1
|
+
"""CLI environment and platform preflight checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from platform_cli.errors import CLIError
|
|
10
|
+
from platform_cli.output import print_json, table
|
|
11
|
+
from platform_cli.runtime import Runtime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def doctor(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
raw: Annotated[bool, typer.Option("--json", help="Print machine-readable results.")] = False,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Check local CLI config, API reachability, and auth."""
|
|
19
|
+
runtime: Runtime = ctx.obj
|
|
20
|
+
checks: list[dict[str, str]] = []
|
|
21
|
+
|
|
22
|
+
checks.append(_config_check(runtime))
|
|
23
|
+
checks.append(_base_url_check(runtime))
|
|
24
|
+
checks.append(_api_key_check(runtime))
|
|
25
|
+
|
|
26
|
+
with runtime.client() as client:
|
|
27
|
+
try:
|
|
28
|
+
ready = client.public("GET", "/health/ready")
|
|
29
|
+
checks.append(
|
|
30
|
+
_check(
|
|
31
|
+
"api_ready",
|
|
32
|
+
"PASS",
|
|
33
|
+
_status_detail(ready),
|
|
34
|
+
"The platform API answered /health/ready.",
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
except CLIError as exc:
|
|
38
|
+
checks.append(
|
|
39
|
+
_check(
|
|
40
|
+
"api_ready",
|
|
41
|
+
"FAIL",
|
|
42
|
+
str(exc),
|
|
43
|
+
"Check --base-url or GENAUG_ADMIN_BASE_URL, then retry.",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if runtime.config.api_key:
|
|
48
|
+
try:
|
|
49
|
+
identity = client.admin("GET", "/me")
|
|
50
|
+
project_ids = identity.get("project_ids", []) if isinstance(identity, dict) else []
|
|
51
|
+
detail = (
|
|
52
|
+
f"auth_method={identity.get('auth_method', 'unknown')}, "
|
|
53
|
+
f"projects={len(project_ids or [])}"
|
|
54
|
+
if isinstance(identity, dict)
|
|
55
|
+
else "authenticated"
|
|
56
|
+
)
|
|
57
|
+
checks.append(
|
|
58
|
+
_check(
|
|
59
|
+
"auth",
|
|
60
|
+
"PASS",
|
|
61
|
+
detail,
|
|
62
|
+
"The configured key can call the admin API.",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
except CLIError as exc:
|
|
66
|
+
checks.append(
|
|
67
|
+
_check(
|
|
68
|
+
"auth",
|
|
69
|
+
"FAIL",
|
|
70
|
+
str(exc),
|
|
71
|
+
"Run genaug auth login with a valid key or fix API key env overrides.",
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
checks.append(
|
|
76
|
+
_check(
|
|
77
|
+
"auth",
|
|
78
|
+
"FAIL",
|
|
79
|
+
"No API key configured.",
|
|
80
|
+
"Run genaug auth login or set GENAUG_ADMIN_API_KEY.",
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
summary = {"verdict": _verdict(checks), "checks": checks}
|
|
85
|
+
if raw:
|
|
86
|
+
print_json(summary)
|
|
87
|
+
else:
|
|
88
|
+
table(
|
|
89
|
+
"General Augment Doctor",
|
|
90
|
+
["Check", "Status", "Detail", "Next action"],
|
|
91
|
+
[
|
|
92
|
+
[item["name"], item["status"], item["detail"], item["next_action"]]
|
|
93
|
+
for item in checks
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if summary["verdict"] == "FAIL":
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _config_check(runtime: Runtime) -> dict[str, str]:
|
|
102
|
+
"""Return the config-file check."""
|
|
103
|
+
if runtime.loaded_config_path.exists():
|
|
104
|
+
return _check(
|
|
105
|
+
"config",
|
|
106
|
+
"PASS",
|
|
107
|
+
f"loaded={runtime.loaded_config_path}",
|
|
108
|
+
"No action needed.",
|
|
109
|
+
)
|
|
110
|
+
return _check(
|
|
111
|
+
"config",
|
|
112
|
+
"WARN",
|
|
113
|
+
f"no saved config at {runtime.loaded_config_path}",
|
|
114
|
+
"Run genaug auth login to persist config, or keep using env overrides.",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _base_url_check(runtime: Runtime) -> dict[str, str]:
|
|
119
|
+
"""Return a base URL sanity check."""
|
|
120
|
+
base_url = runtime.config.base_url.rstrip("/")
|
|
121
|
+
if base_url.startswith(("http://", "https://")):
|
|
122
|
+
return _check("base_url", "PASS", base_url, "No action needed.")
|
|
123
|
+
return _check(
|
|
124
|
+
"base_url",
|
|
125
|
+
"FAIL",
|
|
126
|
+
base_url or "<empty>",
|
|
127
|
+
"Set --base-url or GENAUG_ADMIN_BASE_URL to an http(s) URL.",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _api_key_check(runtime: Runtime) -> dict[str, str]:
|
|
132
|
+
"""Return an API-key presence check without printing the key."""
|
|
133
|
+
if runtime.config.api_key:
|
|
134
|
+
return _check("api_key", "PASS", "configured", "No action needed.")
|
|
135
|
+
return _check(
|
|
136
|
+
"api_key",
|
|
137
|
+
"FAIL",
|
|
138
|
+
"missing",
|
|
139
|
+
"Run genaug auth login or set GENAUG_ADMIN_API_KEY.",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _status_detail(payload: Any) -> str:
|
|
144
|
+
"""Format a compact health-check detail."""
|
|
145
|
+
if isinstance(payload, dict):
|
|
146
|
+
status = payload.get("status") or payload.get("state") or "unknown"
|
|
147
|
+
db = payload.get("db")
|
|
148
|
+
redis = payload.get("redis")
|
|
149
|
+
dependencies = ", ".join(str(item) for item in (db, redis) if item)
|
|
150
|
+
return f"status={status}" + (f", {dependencies}" if dependencies else "")
|
|
151
|
+
return str(payload)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _check(name: str, status: str, detail: str, next_action: str) -> dict[str, str]:
|
|
155
|
+
"""Return one doctor check row."""
|
|
156
|
+
return {
|
|
157
|
+
"name": name,
|
|
158
|
+
"status": status,
|
|
159
|
+
"detail": detail,
|
|
160
|
+
"next_action": next_action,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _verdict(checks: list[dict[str, str]]) -> str:
|
|
165
|
+
"""Return the aggregate doctor verdict."""
|
|
166
|
+
if any(item["status"] == "FAIL" for item in checks):
|
|
167
|
+
return "FAIL"
|
|
168
|
+
if any(item["status"] == "WARN" for item in checks):
|
|
169
|
+
return "WARN"
|
|
170
|
+
return "PASS"
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Tenant identity-link 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 identity links.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_identity_links(
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
20
|
+
limit: Annotated[int, typer.Option(min=1, max=1000, help="Maximum links to return.")] = 100,
|
|
21
|
+
offset: Annotated[int, typer.Option(min=0, help="Pagination offset.")] = 0,
|
|
22
|
+
json_output: Annotated[
|
|
23
|
+
bool,
|
|
24
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
25
|
+
] = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""List verified and pending identity links for one project."""
|
|
28
|
+
runtime: Runtime = ctx.obj
|
|
29
|
+
with runtime.client() as client:
|
|
30
|
+
project_payload = resolve_project(client, project)
|
|
31
|
+
response = client.admin(
|
|
32
|
+
"GET",
|
|
33
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/identity-links",
|
|
34
|
+
params={"limit": limit, "offset": offset},
|
|
35
|
+
)
|
|
36
|
+
if json_output:
|
|
37
|
+
print_json(response)
|
|
38
|
+
return
|
|
39
|
+
items = response.get("items", []) if isinstance(response, dict) else []
|
|
40
|
+
rows = [_identity_row(item) for item in items if isinstance(item, dict)]
|
|
41
|
+
table(
|
|
42
|
+
"Identity links",
|
|
43
|
+
["Phone", "Provider", "Provider User", "Verified", "Linked At"],
|
|
44
|
+
rows,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("create-test")
|
|
49
|
+
def create_test_identity_link(
|
|
50
|
+
ctx: typer.Context,
|
|
51
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
52
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to link.")],
|
|
53
|
+
provider_user_id: Annotated[
|
|
54
|
+
str,
|
|
55
|
+
typer.Option("--provider-user-id", help="Tenant app user id for this link."),
|
|
56
|
+
],
|
|
57
|
+
provider_name: Annotated[
|
|
58
|
+
str,
|
|
59
|
+
typer.Option("--provider-name", help="Tenant identity provider name."),
|
|
60
|
+
] = "app",
|
|
61
|
+
metadata: Annotated[
|
|
62
|
+
list[str] | None,
|
|
63
|
+
typer.Option("--metadata", help="Metadata as key=value. Repeatable."),
|
|
64
|
+
] = None,
|
|
65
|
+
json_output: Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
68
|
+
] = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Create or update one verified test identity link."""
|
|
71
|
+
payload = {
|
|
72
|
+
"phone_e164": phone,
|
|
73
|
+
"provider_user_id": provider_user_id,
|
|
74
|
+
"provider_name": provider_name,
|
|
75
|
+
"metadata": _metadata_pairs(metadata or []),
|
|
76
|
+
}
|
|
77
|
+
runtime: Runtime = ctx.obj
|
|
78
|
+
with runtime.client() as client:
|
|
79
|
+
project_payload = resolve_project(client, project)
|
|
80
|
+
response = client.admin(
|
|
81
|
+
"POST",
|
|
82
|
+
f"/projects/{encode_path_segment(str(project_payload['id']))}/identity-links/test",
|
|
83
|
+
json=payload,
|
|
84
|
+
)
|
|
85
|
+
if json_output:
|
|
86
|
+
print_json(response)
|
|
87
|
+
return
|
|
88
|
+
table(
|
|
89
|
+
"Created test identity link",
|
|
90
|
+
["Field", "Value"],
|
|
91
|
+
[
|
|
92
|
+
["Phone", _value(response, "phone_e164") or phone],
|
|
93
|
+
["Provider", _value(response, "provider_name") or provider_name],
|
|
94
|
+
["Provider User", _value(response, "provider_user_id") or provider_user_id],
|
|
95
|
+
["Verified", _value(response, "verified")],
|
|
96
|
+
["Linked At", _value(response, "linked_at")],
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("link-user")
|
|
102
|
+
def link_user(
|
|
103
|
+
ctx: typer.Context,
|
|
104
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
105
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to link.")],
|
|
106
|
+
provider_user_id: Annotated[
|
|
107
|
+
str,
|
|
108
|
+
typer.Option("--provider-user-id", help="Tenant app user id for this link."),
|
|
109
|
+
],
|
|
110
|
+
provider_name: Annotated[
|
|
111
|
+
str,
|
|
112
|
+
typer.Option("--provider-name", help="Tenant identity provider name."),
|
|
113
|
+
] = "app",
|
|
114
|
+
metadata: Annotated[
|
|
115
|
+
list[str] | None,
|
|
116
|
+
typer.Option("--metadata", help="Metadata as key=value. Repeatable."),
|
|
117
|
+
] = None,
|
|
118
|
+
json_output: Annotated[
|
|
119
|
+
bool,
|
|
120
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
121
|
+
] = False,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Create an app-initiated OTP identity-link challenge."""
|
|
124
|
+
response = _identity_challenge(
|
|
125
|
+
ctx,
|
|
126
|
+
project=project,
|
|
127
|
+
endpoint="link-user",
|
|
128
|
+
payload={
|
|
129
|
+
"phone_e164": phone,
|
|
130
|
+
"provider_user_id": provider_user_id,
|
|
131
|
+
"provider_name": provider_name,
|
|
132
|
+
"metadata": _metadata_pairs(metadata or []),
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
_print_challenge(
|
|
136
|
+
response,
|
|
137
|
+
title="Identity link challenge",
|
|
138
|
+
fallback_phone=phone,
|
|
139
|
+
fallback_provider=provider_name,
|
|
140
|
+
fallback_provider_user_id=provider_user_id,
|
|
141
|
+
json_output=json_output,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command("verification-code")
|
|
146
|
+
def verification_code(
|
|
147
|
+
ctx: typer.Context,
|
|
148
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
149
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to link.")],
|
|
150
|
+
provider_user_id: Annotated[
|
|
151
|
+
str,
|
|
152
|
+
typer.Option("--provider-user-id", help="Tenant app user id for this link."),
|
|
153
|
+
],
|
|
154
|
+
provider_name: Annotated[
|
|
155
|
+
str,
|
|
156
|
+
typer.Option("--provider-name", help="Tenant identity provider name."),
|
|
157
|
+
] = "app",
|
|
158
|
+
metadata: Annotated[
|
|
159
|
+
list[str] | None,
|
|
160
|
+
typer.Option("--metadata", help="Metadata as key=value. Repeatable."),
|
|
161
|
+
] = None,
|
|
162
|
+
json_output: Annotated[
|
|
163
|
+
bool,
|
|
164
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
165
|
+
] = False,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Create a code that the tenant app can show to the user."""
|
|
168
|
+
response = _identity_challenge(
|
|
169
|
+
ctx,
|
|
170
|
+
project=project,
|
|
171
|
+
endpoint="verification-code",
|
|
172
|
+
payload={
|
|
173
|
+
"phone_e164": phone,
|
|
174
|
+
"provider_user_id": provider_user_id,
|
|
175
|
+
"provider_name": provider_name,
|
|
176
|
+
"metadata": _metadata_pairs(metadata or []),
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
_print_challenge(
|
|
180
|
+
response,
|
|
181
|
+
title="Identity verification code",
|
|
182
|
+
fallback_phone=phone,
|
|
183
|
+
fallback_provider=provider_name,
|
|
184
|
+
fallback_provider_user_id=provider_user_id,
|
|
185
|
+
json_output=json_output,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command("magic-link")
|
|
190
|
+
def magic_link(
|
|
191
|
+
ctx: typer.Context,
|
|
192
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
193
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to link.")],
|
|
194
|
+
user_identifier: Annotated[
|
|
195
|
+
str,
|
|
196
|
+
typer.Option("--user-identifier", help="Email or app username to prefill."),
|
|
197
|
+
],
|
|
198
|
+
provider_name: Annotated[
|
|
199
|
+
str,
|
|
200
|
+
typer.Option("--provider-name", help="Tenant identity provider name."),
|
|
201
|
+
] = "app",
|
|
202
|
+
channel: Annotated[
|
|
203
|
+
str,
|
|
204
|
+
typer.Option("--channel", help="Delivery channel: whatsapp, sms, or telegram."),
|
|
205
|
+
] = "whatsapp",
|
|
206
|
+
metadata: Annotated[
|
|
207
|
+
list[str] | None,
|
|
208
|
+
typer.Option("--metadata", help="Metadata as key=value. Repeatable."),
|
|
209
|
+
] = None,
|
|
210
|
+
json_output: Annotated[
|
|
211
|
+
bool,
|
|
212
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
213
|
+
] = False,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Create and optionally deliver an agent-initiated magic-link challenge."""
|
|
216
|
+
response = _identity_challenge(
|
|
217
|
+
ctx,
|
|
218
|
+
project=project,
|
|
219
|
+
endpoint="magic-link",
|
|
220
|
+
payload={
|
|
221
|
+
"phone_e164": phone,
|
|
222
|
+
"user_identifier": user_identifier,
|
|
223
|
+
"provider_name": provider_name,
|
|
224
|
+
"channel": channel,
|
|
225
|
+
"metadata": _metadata_pairs(metadata or []),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
_print_challenge(
|
|
229
|
+
response,
|
|
230
|
+
title="Identity magic link",
|
|
231
|
+
fallback_phone=phone,
|
|
232
|
+
fallback_provider=provider_name,
|
|
233
|
+
fallback_provider_user_id=user_identifier,
|
|
234
|
+
json_output=json_output,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command("verify")
|
|
239
|
+
def verify_identity(
|
|
240
|
+
ctx: typer.Context,
|
|
241
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
242
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to verify.")],
|
|
243
|
+
code: Annotated[str, typer.Option("--code", help="OTP, texted code, or magic-link state.")],
|
|
244
|
+
provider_name: Annotated[
|
|
245
|
+
str,
|
|
246
|
+
typer.Option("--provider-name", help="Tenant identity provider name."),
|
|
247
|
+
] = "app",
|
|
248
|
+
provider_user_id: Annotated[
|
|
249
|
+
str | None,
|
|
250
|
+
typer.Option("--provider-user-id", help="Override app user id after Auth0 callback."),
|
|
251
|
+
] = None,
|
|
252
|
+
json_output: Annotated[
|
|
253
|
+
bool,
|
|
254
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
255
|
+
] = False,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Verify a pending identity-link challenge."""
|
|
258
|
+
runtime: Runtime = ctx.obj
|
|
259
|
+
with runtime.client() as client:
|
|
260
|
+
project_payload = resolve_project(client, project)
|
|
261
|
+
payload = {
|
|
262
|
+
"phone_e164": phone,
|
|
263
|
+
"provider_name": provider_name,
|
|
264
|
+
"code": code,
|
|
265
|
+
}
|
|
266
|
+
if provider_user_id:
|
|
267
|
+
payload["provider_user_id"] = provider_user_id
|
|
268
|
+
response = client.integrations(
|
|
269
|
+
"POST",
|
|
270
|
+
f"/{encode_path_segment(str(project_payload['id']))}/verify",
|
|
271
|
+
json=payload,
|
|
272
|
+
)
|
|
273
|
+
_print_resolution(response, json_output=json_output, success_message="Identity verified")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command("resolve")
|
|
277
|
+
def resolve_identity(
|
|
278
|
+
ctx: typer.Context,
|
|
279
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
280
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to resolve.")],
|
|
281
|
+
provider_name: Annotated[
|
|
282
|
+
str | None,
|
|
283
|
+
typer.Option("--provider-name", help="Optional tenant identity provider name."),
|
|
284
|
+
] = None,
|
|
285
|
+
json_output: Annotated[
|
|
286
|
+
bool,
|
|
287
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
288
|
+
] = False,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Resolve a verified phone-to-app-account identity link."""
|
|
291
|
+
runtime: Runtime = ctx.obj
|
|
292
|
+
with runtime.client() as client:
|
|
293
|
+
project_payload = resolve_project(client, project)
|
|
294
|
+
params = {"provider_name": provider_name} if provider_name else None
|
|
295
|
+
response = client.integrations(
|
|
296
|
+
"GET",
|
|
297
|
+
f"/{encode_path_segment(str(project_payload['id']))}/resolve/{encode_path_segment(phone)}",
|
|
298
|
+
params=params,
|
|
299
|
+
)
|
|
300
|
+
_print_resolution(response, json_output=json_output, success_message="Identity resolved")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@app.command("unlink")
|
|
304
|
+
def unlink_identity(
|
|
305
|
+
ctx: typer.Context,
|
|
306
|
+
project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
|
|
307
|
+
phone: Annotated[str, typer.Option("--phone", help="Phone number to unlink.")],
|
|
308
|
+
provider_name: Annotated[
|
|
309
|
+
str,
|
|
310
|
+
typer.Option("--provider-name", help="Tenant identity provider name."),
|
|
311
|
+
],
|
|
312
|
+
yes: Annotated[
|
|
313
|
+
bool,
|
|
314
|
+
typer.Option("--yes", help="Confirm unlinking this identity mapping."),
|
|
315
|
+
] = False,
|
|
316
|
+
json_output: Annotated[
|
|
317
|
+
bool,
|
|
318
|
+
typer.Option("--json", help="Print machine-readable JSON."),
|
|
319
|
+
] = False,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Remove one phone-to-app-account identity link."""
|
|
322
|
+
if not yes and not typer.confirm(f"Unlink {phone} from {provider_name}?"):
|
|
323
|
+
raise typer.Exit(1)
|
|
324
|
+
runtime: Runtime = ctx.obj
|
|
325
|
+
with runtime.client() as client:
|
|
326
|
+
project_payload = resolve_project(client, project)
|
|
327
|
+
response = client.integrations(
|
|
328
|
+
"DELETE",
|
|
329
|
+
f"/{encode_path_segment(str(project_payload['id']))}/unlink/{encode_path_segment(phone)}",
|
|
330
|
+
params={"provider_name": provider_name},
|
|
331
|
+
)
|
|
332
|
+
if json_output:
|
|
333
|
+
print_json(response)
|
|
334
|
+
return
|
|
335
|
+
message = "Identity unlinked" if _value(response, "unlinked") else "Identity link not found"
|
|
336
|
+
print_success(message)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _identity_row(link: dict[str, Any]) -> list[object]:
|
|
340
|
+
"""Return a table row for one identity link."""
|
|
341
|
+
return [
|
|
342
|
+
link.get("phone_e164", ""),
|
|
343
|
+
link.get("provider_name", ""),
|
|
344
|
+
link.get("provider_user_id", ""),
|
|
345
|
+
"yes" if link.get("verified") else "no",
|
|
346
|
+
link.get("linked_at", ""),
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _identity_challenge(
|
|
351
|
+
ctx: typer.Context,
|
|
352
|
+
*,
|
|
353
|
+
project: str,
|
|
354
|
+
endpoint: str,
|
|
355
|
+
payload: dict[str, Any],
|
|
356
|
+
) -> object:
|
|
357
|
+
"""Create one identity challenge through the integration API."""
|
|
358
|
+
runtime: Runtime = ctx.obj
|
|
359
|
+
with runtime.client() as client:
|
|
360
|
+
project_payload = resolve_project(client, project)
|
|
361
|
+
return client.integrations(
|
|
362
|
+
"POST",
|
|
363
|
+
f"/{encode_path_segment(str(project_payload['id']))}/{endpoint}",
|
|
364
|
+
json=payload,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _print_challenge(
|
|
369
|
+
response: object,
|
|
370
|
+
*,
|
|
371
|
+
title: str,
|
|
372
|
+
fallback_phone: str,
|
|
373
|
+
fallback_provider: str,
|
|
374
|
+
fallback_provider_user_id: str,
|
|
375
|
+
json_output: bool,
|
|
376
|
+
) -> None:
|
|
377
|
+
"""Print an identity challenge response."""
|
|
378
|
+
if json_output:
|
|
379
|
+
print_json(response)
|
|
380
|
+
return
|
|
381
|
+
table(
|
|
382
|
+
title,
|
|
383
|
+
["Field", "Value"],
|
|
384
|
+
[
|
|
385
|
+
["Phone", _value(response, "phone_e164") or fallback_phone],
|
|
386
|
+
["Provider", _value(response, "provider_name") or fallback_provider],
|
|
387
|
+
["Provider User", _value(response, "provider_user_id") or fallback_provider_user_id],
|
|
388
|
+
["Expires", _value(response, "verification_expires_at")],
|
|
389
|
+
["Magic Link", _value(response, "magic_link")],
|
|
390
|
+
["Debug Code", _value(response, "debug_verification_code")],
|
|
391
|
+
],
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _print_resolution(
|
|
396
|
+
response: object,
|
|
397
|
+
*,
|
|
398
|
+
json_output: bool,
|
|
399
|
+
success_message: str,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Print an identity resolution response."""
|
|
402
|
+
if json_output:
|
|
403
|
+
print_json(response)
|
|
404
|
+
return
|
|
405
|
+
print_success(success_message)
|
|
406
|
+
table(
|
|
407
|
+
"Identity resolution",
|
|
408
|
+
["Field", "Value"],
|
|
409
|
+
[
|
|
410
|
+
["Phone", _value(response, "phone_e164")],
|
|
411
|
+
["Provider", _value(response, "provider_name")],
|
|
412
|
+
["Provider User", _value(response, "provider_user_id")],
|
|
413
|
+
["Linked At", _value(response, "linked_at")],
|
|
414
|
+
],
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _metadata_pairs(values: list[str]) -> dict[str, str]:
|
|
419
|
+
"""Parse repeated key=value metadata flags."""
|
|
420
|
+
parsed: dict[str, str] = {}
|
|
421
|
+
for item in values:
|
|
422
|
+
key, separator, value = item.partition("=")
|
|
423
|
+
if not separator or not key.strip():
|
|
424
|
+
raise typer.BadParameter("--metadata values must use key=value.")
|
|
425
|
+
parsed[key.strip()] = value
|
|
426
|
+
return parsed
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _value(payload: object, key: str) -> object:
|
|
430
|
+
"""Safely read a value from a response mapping."""
|
|
431
|
+
if isinstance(payload, dict):
|
|
432
|
+
return payload.get(key, "")
|
|
433
|
+
return ""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Starter agent scaffold 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.errors import CLIError
|
|
11
|
+
from platform_cli.openapi import scaffold_basic_agent
|
|
12
|
+
from platform_cli.output import print_success, print_warning, table
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init(
|
|
16
|
+
name: Annotated[str, typer.Argument(help="Agent/project name, such as dayplan.")],
|
|
17
|
+
output_dir: Annotated[Path | None, typer.Option(help="Output directory.")] = None,
|
|
18
|
+
display_name: Annotated[str | None, typer.Option(help="Tenant-facing display name.")] = None,
|
|
19
|
+
description: Annotated[
|
|
20
|
+
str | None,
|
|
21
|
+
typer.Option(help="Agent purpose shown in SOUL.md and the handoff prompt."),
|
|
22
|
+
] = None,
|
|
23
|
+
tool: Annotated[
|
|
24
|
+
list[str] | None,
|
|
25
|
+
typer.Option("--tool", help="Builtin tool ID to enable, for example web_search."),
|
|
26
|
+
] = None,
|
|
27
|
+
force: Annotated[bool, typer.Option(help="Overwrite existing starter files.")] = False,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Create a starter genaug-agent.yaml workspace without an OpenAPI spec."""
|
|
30
|
+
try:
|
|
31
|
+
result = scaffold_basic_agent(
|
|
32
|
+
name=name,
|
|
33
|
+
output_dir=output_dir,
|
|
34
|
+
display_name=display_name,
|
|
35
|
+
description=description,
|
|
36
|
+
builtin_tools=tool,
|
|
37
|
+
force=force,
|
|
38
|
+
)
|
|
39
|
+
except FileExistsError as exc:
|
|
40
|
+
raise CLIError(str(exc)) from exc
|
|
41
|
+
rows: list[list[object]] = [
|
|
42
|
+
["Manifest", result.config_path],
|
|
43
|
+
["Personality", result.soul_path],
|
|
44
|
+
["Skills", result.skills_dir],
|
|
45
|
+
["Tools", result.tools_dir],
|
|
46
|
+
["Handoff", result.agent_prompt_path],
|
|
47
|
+
]
|
|
48
|
+
table("Starter agent scaffold", ["File", "Path"], rows)
|
|
49
|
+
print_success(f"Generated starter agent in {result.root}")
|
|
50
|
+
if result.builtin_tools:
|
|
51
|
+
print_success(f"Enabled builtin tools: {', '.join(result.builtin_tools)}")
|
|
52
|
+
else:
|
|
53
|
+
print_warning("No builtin tools enabled yet. Use --tool or genaug tools toggle later.")
|
|
54
|
+
typer.echo(f"Next: genaug dev {result.config_path} --message \"What can you help me with?\"")
|
|
55
|
+
typer.echo(f"Then: genaug deploy {result.config_path}")
|