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,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)}.")
|