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,174 @@
1
+ """Observability and support-evidence commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from platform_cli.client import encode_path_segment, resolve_project
12
+ from platform_cli.output import print_json, print_success, table
13
+ from platform_cli.runtime import Runtime
14
+
15
+ app = typer.Typer(help="Inspect traces and support evidence.")
16
+
17
+
18
+ @app.command("trace")
19
+ def get_trace(
20
+ ctx: typer.Context,
21
+ trace_id: Annotated[str, typer.Argument(help="Trace id returned by /v1/responses.")],
22
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
23
+ json_output: Annotated[
24
+ bool,
25
+ typer.Option("--json", help="Print machine-readable JSON."),
26
+ ] = False,
27
+ ) -> None:
28
+ """Fetch one project-scoped assistant trace."""
29
+ runtime: Runtime = ctx.obj
30
+ with runtime.client() as client:
31
+ project_payload = resolve_project(client, project)
32
+ response = client.admin(
33
+ "GET",
34
+ (
35
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/traces/"
36
+ f"{encode_path_segment(trace_id)}"
37
+ ),
38
+ )
39
+ if json_output:
40
+ print_json(response)
41
+ return
42
+ table(
43
+ "Trace",
44
+ ["Field", "Value"],
45
+ [
46
+ ["Trace ID", _value(response, "trace_id") or trace_id],
47
+ ["Response", _value(response, "id")],
48
+ ["User", _value(response, "user_id")],
49
+ ["Session", _value(response, "session_id")],
50
+ ["Model", _value(response, "model_used")],
51
+ ["Input Tokens", _value(response, "input_tokens")],
52
+ ["Output Tokens", _value(response, "output_tokens")],
53
+ ["Cost USD", _value(response, "cost_usd")],
54
+ ["Latency MS", _value(response, "latency_ms")],
55
+ ["Langfuse", _value(response, "langfuse_url")],
56
+ ],
57
+ )
58
+
59
+
60
+ @app.command("support-bundle")
61
+ def support_bundle(
62
+ ctx: typer.Context,
63
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
64
+ trace_id: Annotated[
65
+ str | None,
66
+ typer.Option("--trace-id", help="Filter by General Augment trace id."),
67
+ ] = None,
68
+ response_id: Annotated[
69
+ str | None,
70
+ typer.Option("--response-id", help="Filter by Responses API response id."),
71
+ ] = None,
72
+ user_id: Annotated[
73
+ str | None,
74
+ typer.Option("--user-id", help="Filter by General Augment user id."),
75
+ ] = None,
76
+ session_id: Annotated[
77
+ str | None,
78
+ typer.Option("--session-id", help="Filter by General Augment session id."),
79
+ ] = None,
80
+ feature: Annotated[
81
+ str | None,
82
+ typer.Option(help="Filter by response metadata feature."),
83
+ ] = None,
84
+ source: Annotated[
85
+ str | None,
86
+ typer.Option(help="Filter by response metadata source."),
87
+ ] = None,
88
+ status: Annotated[
89
+ str | None,
90
+ typer.Option(help="Filter by success/failure status."),
91
+ ] = None,
92
+ start: Annotated[
93
+ str | None,
94
+ typer.Option(help="Filter start timestamp, ISO 8601."),
95
+ ] = None,
96
+ end: Annotated[
97
+ str | None,
98
+ typer.Option(help="Filter end timestamp, ISO 8601."),
99
+ ] = None,
100
+ limit: Annotated[int, typer.Option(min=1, max=200, help="Maximum rows per section.")] = 50,
101
+ output: Annotated[
102
+ Path | None,
103
+ typer.Option("--output", "-o", help="Write support bundle JSON to a file."),
104
+ ] = None,
105
+ json_output: Annotated[
106
+ bool,
107
+ typer.Option("--json", help="Print machine-readable JSON."),
108
+ ] = False,
109
+ ) -> None:
110
+ """Export a bounded project support bundle."""
111
+ params = _support_params(
112
+ limit=limit,
113
+ trace_id=trace_id,
114
+ response_id=response_id,
115
+ user_id=user_id,
116
+ session_id=session_id,
117
+ feature=feature,
118
+ source=source,
119
+ status=status,
120
+ start=start,
121
+ end=end,
122
+ )
123
+ runtime: Runtime = ctx.obj
124
+ with runtime.client() as client:
125
+ project_payload = resolve_project(client, project)
126
+ response = client.admin(
127
+ "GET",
128
+ (
129
+ f"/projects/{encode_path_segment(str(project_payload['id']))}"
130
+ "/observability/support-bundle"
131
+ ),
132
+ params=params,
133
+ )
134
+ if output is not None:
135
+ output.write_text(json.dumps(response, indent=2, sort_keys=True) + "\n", encoding="utf-8")
136
+ print_success(f"Wrote support bundle to {output}.")
137
+ return
138
+ if json_output:
139
+ print_json(response)
140
+ return
141
+ metrics = response.get("metrics", {}) if isinstance(response, dict) else {}
142
+ table(
143
+ "Support bundle",
144
+ ["Field", "Value"],
145
+ [
146
+ ["Project", _value(response, "project_id")],
147
+ ["Generated At", _value(response, "generated_at")],
148
+ ["Traces", _metric(metrics, "trace_count")],
149
+ ["Logs", _metric(metrics, "log_count")],
150
+ ["Audit Events", _metric(metrics, "audit_event_count")],
151
+ ["Memory Facts", _metric(metrics, "memory_fact_count")],
152
+ ["Usage Events", _metric(metrics, "usage_event_count")],
153
+ ["Timeline Events", _metric(metrics, "timeline_event_count")],
154
+ ],
155
+ )
156
+
157
+
158
+ def _support_params(**values: object) -> dict[str, object]:
159
+ """Return support-bundle query params without unset filters."""
160
+ return {key: value for key, value in values.items() if value is not None}
161
+
162
+
163
+ def _metric(payload: object, key: str) -> object:
164
+ """Read a metric from the support-bundle metrics map."""
165
+ if isinstance(payload, dict):
166
+ return payload.get(key, 0)
167
+ return 0
168
+
169
+
170
+ def _value(payload: object, key: str) -> object:
171
+ """Safely read a value from a response mapping."""
172
+ if isinstance(payload, dict):
173
+ return payload.get(key, "")
174
+ return ""
@@ -0,0 +1,72 @@
1
+ """App-developer onboarding commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import typer
8
+
9
+ from platform_cli.commands.verify import build_project_verification_payload
10
+ from platform_cli.errors import CLIError
11
+ from platform_cli.output import panel, print_json, table
12
+ from platform_cli.runtime import Runtime
13
+
14
+ app = typer.Typer(help="Verify app-developer onboarding before launch.")
15
+
16
+
17
+ @app.command("verify")
18
+ def verify_onboarding(
19
+ ctx: typer.Context,
20
+ project: str = typer.Option(..., help="Project id, slug, or name."),
21
+ message: str = typer.Option(
22
+ "Reply with one short sentence confirming this General Augment project works.",
23
+ help="Message for the hosted agent test.",
24
+ ),
25
+ user: str = typer.Option(
26
+ "genaug-onboarding-user",
27
+ help="Synthetic app user id for memory and agent checks.",
28
+ ),
29
+ phone_e164: str = typer.Option("+15550000000", help="Synthetic E.164 user identity."),
30
+ channel: str = typer.Option("sms", help="Synthetic channel: sms, whatsapp, ios, or telegram."),
31
+ dashboard_url: str = typer.Option(
32
+ os.getenv("GENAUG_DASHBOARD_URL", "https://app.generalaugment.com"),
33
+ help="Dashboard base URL for follow-up UI checks.",
34
+ ),
35
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
36
+ ) -> None:
37
+ """Run the one-command onboarding gate for an existing project."""
38
+
39
+ runtime: Runtime = ctx.obj
40
+ with runtime.client() as client:
41
+ payload = build_project_verification_payload(
42
+ client,
43
+ project=project,
44
+ message=message,
45
+ user=user,
46
+ phone_e164=phone_e164,
47
+ channel=channel,
48
+ dashboard_url=dashboard_url,
49
+ )
50
+ payload["onboarding"] = {
51
+ "verdict": payload["verdict"],
52
+ "required_follow_up": [
53
+ "Confirm the dashboard shows the same project, tools, usage, traces, logs, and memory.",
54
+ "Keep project API keys server-side in the app backend.",
55
+ "Handle 402 and 429 responses before production traffic.",
56
+ ],
57
+ }
58
+ if json_output:
59
+ print_json(payload)
60
+ else:
61
+ table(
62
+ f"Onboarding Verify: {payload['project']['slug']}",
63
+ ["Check", "Status", "Detail"],
64
+ [[item["name"], item["status"], item["detail"]] for item in payload["checks"]],
65
+ )
66
+ panel(
67
+ "Dashboard Follow-up",
68
+ "\n".join(f"{key}: {value}" for key, value in payload["dashboard"].items()),
69
+ )
70
+ if payload["verdict"] != "PASS":
71
+ failed = ", ".join(item["name"] for item in payload["checks"] if item["status"] != "PASS")
72
+ raise CLIError(f"Onboarding verification failed: {failed}")
@@ -0,0 +1,302 @@
1
+ """Project management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from platform_cli.client import encode_path_segment, resolve_project
12
+ from platform_cli.output import print_json, print_success, table
13
+ from platform_cli.runtime import Runtime
14
+
15
+ app = typer.Typer(help="Manage projects.")
16
+
17
+
18
+ @app.command("list")
19
+ def list_projects(ctx: typer.Context) -> None:
20
+ """List visible projects."""
21
+ runtime: Runtime = ctx.obj
22
+ with runtime.client() as client:
23
+ payload = client.admin("GET", "/projects")
24
+ items = payload.get("items", []) if isinstance(payload, dict) else []
25
+ rows = [
26
+ [item.get("name", ""), item.get("slug", ""), item.get("status", ""), item.get("id", "")]
27
+ for item in items
28
+ if isinstance(item, dict)
29
+ ]
30
+ table("Projects", ["Name", "Slug", "Status", "ID"], rows)
31
+
32
+
33
+ @app.command("create")
34
+ def create_project(
35
+ ctx: typer.Context,
36
+ name: str = typer.Option(..., help="Project display name."),
37
+ slug: str = typer.Option(..., help="Project slug."),
38
+ system_prompt: str = typer.Option("You are a helpful agent.", help="Initial system prompt."),
39
+ ) -> None:
40
+ """Create a project."""
41
+ runtime: Runtime = ctx.obj
42
+ payload = {
43
+ "name": name,
44
+ "slug": slug,
45
+ "system_prompt": system_prompt,
46
+ }
47
+ with runtime.client() as client:
48
+ project = client.admin("POST", "/projects", json=payload)
49
+ print_success(f"Created project {project.get('name', name)} ({project.get('id', 'unknown')}).")
50
+
51
+
52
+ @app.command("usage")
53
+ def project_usage(
54
+ ctx: typer.Context,
55
+ project: str = typer.Option(..., help="Project id, slug, or name."),
56
+ start_date: str | None = typer.Option(None, help="Inclusive start date, YYYY-MM-DD."),
57
+ end_date: str | None = typer.Option(None, help="Inclusive end date, YYYY-MM-DD."),
58
+ ) -> None:
59
+ """Show project usage and billing aggregates."""
60
+ runtime: Runtime = ctx.obj
61
+ params = {
62
+ key: value
63
+ for key, value in {"start_date": start_date, "end_date": end_date}.items()
64
+ if value is not None
65
+ }
66
+ with runtime.client() as client:
67
+ project_payload = resolve_project(client, project)
68
+ usage = client.admin(
69
+ "GET",
70
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/usage",
71
+ params=params,
72
+ )
73
+ totals = usage.get("totals", {}) if isinstance(usage, dict) else {}
74
+ rows: list[list[object]] = [
75
+ ["Agent turns", _metric(totals, "agent_turns_count")],
76
+ ["Stored messages", _metric(totals, "messages_count")],
77
+ ["Tool calls", _metric(totals, "tool_calls_count")],
78
+ ["Cost USD", _metric(totals, "total_cost_usd")],
79
+ ]
80
+ table(f"Usage for {project_payload.get('slug', project)}", ["Metric", "Value"], rows)
81
+
82
+ days = usage.get("days", []) if isinstance(usage, dict) else []
83
+ day_rows = [
84
+ [
85
+ item.get("date", item.get("day", "")),
86
+ _metric(item, "agent_turns_count"),
87
+ _metric(item, "tool_calls_count"),
88
+ _metric(item, "total_cost_usd"),
89
+ ]
90
+ for item in days
91
+ if isinstance(item, dict)
92
+ ]
93
+ if day_rows:
94
+ table("Daily Usage", ["Date", "Agent turns", "Tool calls", "Cost USD"], day_rows)
95
+
96
+
97
+ @app.command("runtime-policy")
98
+ def project_runtime_policy(
99
+ ctx: typer.Context,
100
+ project: str = typer.Option(..., help="Project id, slug, or name."),
101
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
102
+ ) -> None:
103
+ """Show the tenant-governed Hermes runtime policy."""
104
+ runtime: Runtime = ctx.obj
105
+ with runtime.client() as client:
106
+ project_payload = resolve_project(client, project)
107
+ policy = client.admin(
108
+ "GET",
109
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/runtime-policy",
110
+ )
111
+ if json_output:
112
+ print_json(policy)
113
+ return
114
+ routing = policy.get("model_routing", {}) if isinstance(policy, dict) else {}
115
+ tiers = routing.get("tiers", {}) if isinstance(routing, dict) else {}
116
+ table(
117
+ f"Runtime Policy for {project_payload.get('slug', project)}",
118
+ ["Surface", "Value"],
119
+ [
120
+ ["Model routing", _model_routing_summary(routing)],
121
+ ["Simple model", _field(tiers, "simple")],
122
+ ["Balanced model", _field(tiers, "balanced")],
123
+ ["Complex model", _field(tiers, "complex")],
124
+ ["Tool discovery", _nested_metric(policy, "tool_discovery", "mode")],
125
+ ["Enabled platform tools", _joined(policy, "platform_tools", "enabled_tool_ids")],
126
+ ["MCP tools", _joined(policy, "mcp", "enabled_tool_ids")],
127
+ ["Skills", _joined(policy, "skills", "names")],
128
+ ],
129
+ )
130
+
131
+
132
+ @app.command("export")
133
+ def export_project(
134
+ ctx: typer.Context,
135
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
136
+ include: Annotated[
137
+ list[str] | None,
138
+ typer.Option("--include", help="Export section to include. Repeatable."),
139
+ ] = None,
140
+ limit: Annotated[int, typer.Option(min=1, max=500, help="Maximum rows per section.")] = 100,
141
+ user_id: Annotated[str | None, typer.Option("--user-id", help="Filter by user id.")] = None,
142
+ session_id: Annotated[
143
+ str | None,
144
+ typer.Option("--session-id", help="Filter by session id."),
145
+ ] = None,
146
+ trace_id: Annotated[str | None, typer.Option("--trace-id", help="Filter by trace id.")] = None,
147
+ start: Annotated[str | None, typer.Option(help="Filter start timestamp, ISO 8601.")] = None,
148
+ end: Annotated[str | None, typer.Option(help="Filter end timestamp, ISO 8601.")] = None,
149
+ output: Annotated[
150
+ Path | None,
151
+ typer.Option("--output", "-o", help="Write export JSON to a file."),
152
+ ] = None,
153
+ json_output: Annotated[
154
+ bool,
155
+ typer.Option("--json", help="Print machine-readable JSON."),
156
+ ] = False,
157
+ ) -> None:
158
+ """Export bounded project data for operator review."""
159
+ runtime: Runtime = ctx.obj
160
+ params = _export_params(
161
+ include=include,
162
+ limit=limit,
163
+ user_id=user_id,
164
+ session_id=session_id,
165
+ trace_id=trace_id,
166
+ start=start,
167
+ end=end,
168
+ )
169
+ with runtime.client() as client:
170
+ project_payload = resolve_project(client, project)
171
+ response = client.admin(
172
+ "GET",
173
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/export",
174
+ params=params,
175
+ )
176
+ if output is not None:
177
+ output.write_text(json.dumps(response, indent=2, sort_keys=True) + "\n", encoding="utf-8")
178
+ print_success(f"Wrote project export to {output}.")
179
+ return
180
+ if json_output:
181
+ print_json(response)
182
+ return
183
+ filters = response.get("filters", {}) if isinstance(response, dict) else {}
184
+ include_sections = filters.get("include", include or []) if isinstance(filters, dict) else []
185
+ table(
186
+ "Project export",
187
+ ["Field", "Value"],
188
+ [
189
+ ["Project", _value(response, "project_id")],
190
+ ["Exported At", _value(response, "exported_at")],
191
+ ["Sections", ", ".join(str(item) for item in include_sections)],
192
+ ["Logs", _section_count(response, "logs")],
193
+ ["Traces", _section_count(response, "traces")],
194
+ ["Audit Events", _section_count(response, "audit_events")],
195
+ ["Memory Facts", _section_count(response, "memory_facts")],
196
+ ["Usage Events", _section_count(response, "usage_events")],
197
+ ],
198
+ )
199
+
200
+
201
+ @app.command("archive")
202
+ def archive_project(
203
+ ctx: typer.Context,
204
+ project: Annotated[str, typer.Argument(help="Project id, slug, or name.")],
205
+ yes: Annotated[
206
+ bool,
207
+ typer.Option("--yes", help="Confirm archiving this project."),
208
+ ] = False,
209
+ json_output: Annotated[
210
+ bool,
211
+ typer.Option("--json", help="Print machine-readable JSON."),
212
+ ] = False,
213
+ ) -> None:
214
+ """Archive one project."""
215
+ if not yes and not typer.confirm(f"Archive project {project}?"):
216
+ raise typer.Exit(1)
217
+ runtime: Runtime = ctx.obj
218
+ with runtime.client() as client:
219
+ project_payload = resolve_project(client, project)
220
+ response = client.admin(
221
+ "POST",
222
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/archive",
223
+ )
224
+ if json_output:
225
+ print_json(response)
226
+ return
227
+ archived_project = response.get("slug", project) if isinstance(response, dict) else project
228
+ print_success(f"Archived project {archived_project}.")
229
+
230
+
231
+ def _metric(payload: object, *keys: str) -> object:
232
+ """Return the first present metric value from a usage payload."""
233
+ if not isinstance(payload, dict):
234
+ return 0
235
+ for key in keys:
236
+ if key in payload:
237
+ return payload[key]
238
+ return 0
239
+
240
+
241
+ def _nested_metric(payload: object, section: str, key: str) -> object:
242
+ """Return a nested field value from a JSON object."""
243
+
244
+ if not isinstance(payload, dict):
245
+ return ""
246
+ nested = payload.get(section)
247
+ if not isinstance(nested, dict):
248
+ return ""
249
+ return nested.get(key, "")
250
+
251
+
252
+ def _field(payload: object, key: str) -> object:
253
+ """Return a field value from a JSON object."""
254
+
255
+ if not isinstance(payload, dict):
256
+ return ""
257
+ return payload.get(key, "")
258
+
259
+
260
+ def _joined(payload: object, section: str, key: str) -> str:
261
+ """Return a compact joined list from a nested JSON object."""
262
+
263
+ if not isinstance(payload, dict):
264
+ return ""
265
+ nested = payload.get(section)
266
+ if not isinstance(nested, dict):
267
+ return ""
268
+ values = nested.get(key)
269
+ if not isinstance(values, list):
270
+ return ""
271
+ return ", ".join(str(item) for item in values) or "none"
272
+
273
+
274
+ def _model_routing_summary(payload: object) -> str:
275
+ """Return a compact model-routing mode summary."""
276
+
277
+ if not isinstance(payload, dict):
278
+ return ""
279
+ mode = str(payload.get("mode") or "")
280
+ default_tier = str(payload.get("default_tier") or "")
281
+ parity = payload.get("channel_parity")
282
+ return f"{mode}, default={default_tier}, channel_parity={parity}"
283
+
284
+
285
+ def _export_params(**values: object) -> dict[str, object]:
286
+ """Return project export query params without unset filters."""
287
+ return {key: value for key, value in values.items() if value is not None}
288
+
289
+
290
+ def _section_count(payload: object, key: str) -> int:
291
+ """Return the length of a list section in a project export."""
292
+ if not isinstance(payload, dict):
293
+ return 0
294
+ value = payload.get(key)
295
+ return len(value) if isinstance(value, list) else 0
296
+
297
+
298
+ def _value(payload: object, key: str) -> object:
299
+ """Safely read a value from a response mapping."""
300
+ if isinstance(payload, dict):
301
+ return payload.get(key, "")
302
+ return ""
@@ -0,0 +1,116 @@
1
+ """Skill management commands."""
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, resolve_project
11
+ from platform_cli.output import print_json, print_success, table
12
+ from platform_cli.runtime import Runtime
13
+
14
+ app = typer.Typer(help="Manage tenant skills.")
15
+
16
+
17
+ @app.command("list")
18
+ def list_skills(
19
+ ctx: typer.Context,
20
+ project: str = typer.Option(..., help="Project id, slug, or name."),
21
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
22
+ ) -> None:
23
+ """List SKILL.md files registered for a tenant."""
24
+ runtime: Runtime = ctx.obj
25
+ with runtime.client() as client:
26
+ project_payload = resolve_project(client, project)
27
+ payload = client.admin(
28
+ "GET",
29
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/skills",
30
+ )
31
+ if json_output:
32
+ print_json(payload)
33
+ return
34
+ items = payload.get("items", []) if isinstance(payload, dict) else []
35
+ rows = [
36
+ [
37
+ item.get("name", ""),
38
+ item.get("description", ""),
39
+ item.get("version", ""),
40
+ ", ".join(str(tag) for tag in item.get("tags", []) or []),
41
+ ", ".join(str(tool) for tool in item.get("tools", []) or []),
42
+ ]
43
+ for item in items
44
+ if isinstance(item, dict)
45
+ ]
46
+ table(
47
+ f"Skills for {project_payload.get('slug', project)}",
48
+ ["Name", "Description", "Version", "Tags", "Tools"],
49
+ rows,
50
+ )
51
+
52
+
53
+ @app.command("view")
54
+ def view_skill(
55
+ ctx: typer.Context,
56
+ skill_name: str = typer.Argument(..., help="Skill name."),
57
+ project: str = typer.Option(..., help="Project id, slug, or name."),
58
+ json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
59
+ ) -> None:
60
+ """Show one tenant SKILL.md file."""
61
+ runtime: Runtime = ctx.obj
62
+ with runtime.client() as client:
63
+ project_payload = resolve_project(client, project)
64
+ payload = client.admin(
65
+ "GET",
66
+ (
67
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/skills/"
68
+ f"{encode_path_segment(skill_name)}"
69
+ ),
70
+ )
71
+ if json_output:
72
+ print_json(payload)
73
+ return
74
+ typer.echo(str(payload.get("content", "")) if isinstance(payload, dict) else "")
75
+
76
+
77
+ @app.command("apply")
78
+ def apply_skill(
79
+ ctx: typer.Context,
80
+ skill_file: Annotated[
81
+ Path,
82
+ typer.Argument(exists=True, dir_okay=False, help="Path to a SKILL.md file."),
83
+ ],
84
+ project: str = typer.Option(..., help="Project id, slug, or name."),
85
+ ) -> None:
86
+ """Create or replace one tenant skill from a local SKILL.md file."""
87
+ runtime: Runtime = ctx.obj
88
+ content = skill_file.read_text(encoding="utf-8")
89
+ with runtime.client() as client:
90
+ project_payload = resolve_project(client, project)
91
+ payload = client.admin(
92
+ "POST",
93
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/skills",
94
+ json={"content": content},
95
+ )
96
+ print_success(f"Applied skill {payload.get('name', skill_file.stem)}.")
97
+
98
+
99
+ @app.command("delete")
100
+ def delete_skill(
101
+ ctx: typer.Context,
102
+ skill_name: str = typer.Argument(..., help="Skill name."),
103
+ project: str = typer.Option(..., help="Project id, slug, or name."),
104
+ ) -> None:
105
+ """Delete one tenant skill."""
106
+ runtime: Runtime = ctx.obj
107
+ with runtime.client() as client:
108
+ project_payload = resolve_project(client, project)
109
+ payload = client.admin(
110
+ "DELETE",
111
+ (
112
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/skills/"
113
+ f"{encode_path_segment(skill_name)}"
114
+ ),
115
+ )
116
+ print_success(f"Deleted skill {payload.get('name', skill_name)}.")