glaip-sdk 0.0.1b10__py3-none-any.whl → 0.0.3__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.
- glaip_sdk/__init__.py +2 -2
- glaip_sdk/_version.py +51 -0
- glaip_sdk/cli/commands/agents.py +201 -109
- glaip_sdk/cli/commands/configure.py +29 -87
- glaip_sdk/cli/commands/init.py +16 -7
- glaip_sdk/cli/commands/mcps.py +73 -153
- glaip_sdk/cli/commands/tools.py +185 -49
- glaip_sdk/cli/main.py +30 -27
- glaip_sdk/cli/utils.py +126 -13
- glaip_sdk/client/__init__.py +54 -2
- glaip_sdk/client/agents.py +175 -237
- glaip_sdk/client/base.py +62 -2
- glaip_sdk/client/mcps.py +63 -20
- glaip_sdk/client/tools.py +95 -28
- glaip_sdk/config/constants.py +10 -3
- glaip_sdk/exceptions.py +13 -0
- glaip_sdk/models.py +20 -4
- glaip_sdk/utils/__init__.py +116 -18
- glaip_sdk/utils/client_utils.py +284 -0
- glaip_sdk/utils/rendering/__init__.py +1 -0
- glaip_sdk/utils/rendering/formatting.py +211 -0
- glaip_sdk/utils/rendering/models.py +53 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
- glaip_sdk/utils/rendering/renderer/base.py +827 -0
- glaip_sdk/utils/rendering/renderer/config.py +33 -0
- glaip_sdk/utils/rendering/renderer/console.py +54 -0
- glaip_sdk/utils/rendering/renderer/debug.py +82 -0
- glaip_sdk/utils/rendering/renderer/panels.py +123 -0
- glaip_sdk/utils/rendering/renderer/progress.py +118 -0
- glaip_sdk/utils/rendering/renderer/stream.py +198 -0
- glaip_sdk/utils/rendering/steps.py +168 -0
- glaip_sdk/utils/run_renderer.py +22 -1086
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/METADATA +9 -37
- glaip_sdk-0.0.3.dist-info/RECORD +40 -0
- glaip_sdk/cli/config.py +0 -592
- glaip_sdk/utils.py +0 -167
- glaip_sdk-0.0.1b10.dist-info/RECORD +0 -28
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.1b10.dist-info → glaip_sdk-0.0.3.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/tools.py
CHANGED
|
@@ -5,19 +5,19 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
from rich.console import Console
|
|
11
12
|
from rich.panel import Panel
|
|
12
13
|
|
|
13
|
-
from glaip_sdk.utils import is_uuid
|
|
14
|
-
|
|
15
14
|
from ..utils import (
|
|
15
|
+
coerce_to_row,
|
|
16
16
|
get_client,
|
|
17
|
-
handle_ambiguous_resource,
|
|
18
17
|
output_flags,
|
|
19
18
|
output_list,
|
|
20
19
|
output_result,
|
|
20
|
+
resolve_resource,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
console = Console()
|
|
@@ -31,25 +31,61 @@ def tools_group():
|
|
|
31
31
|
|
|
32
32
|
def _resolve_tool(ctx, client, ref, select=None):
|
|
33
33
|
"""Resolve tool reference (ID or name) with ambiguity handling."""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
return resolve_resource(
|
|
35
|
+
ctx,
|
|
36
|
+
ref,
|
|
37
|
+
get_by_id=client.get_tool,
|
|
38
|
+
find_by_name=client.find_tools,
|
|
39
|
+
label="Tool",
|
|
40
|
+
select=select,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ----------------------------- Helpers --------------------------------- #
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _extract_internal_name(code: str) -> str:
|
|
48
|
+
"""Extract plugin class name attribute from tool code."""
|
|
49
|
+
m = re.search(r'^\s*name\s*:\s*str\s*=\s*"([^"]+)"', code, re.M)
|
|
50
|
+
if not m:
|
|
51
|
+
m = re.search(r'^\s*name\s*=\s*"([^"]+)"', code, re.M)
|
|
52
|
+
if not m:
|
|
53
|
+
raise click.ClickException(
|
|
54
|
+
"Could not find plugin 'name' attribute in the tool file. "
|
|
55
|
+
'Ensure your plugin class defines e.g. name: str = "my_tool".'
|
|
56
|
+
)
|
|
57
|
+
return m.group(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_name_match(provided: str | None, internal: str) -> str:
|
|
61
|
+
"""Validate provided --name against internal name; return effective name."""
|
|
62
|
+
if provided and provided != internal:
|
|
63
|
+
raise click.ClickException(
|
|
64
|
+
f"--name '{provided}' does not match plugin internal name '{internal}'. "
|
|
65
|
+
"Either update the code or pass a matching --name."
|
|
66
|
+
)
|
|
67
|
+
return provided or internal
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _check_duplicate_name(client, tool_name: str) -> None:
|
|
71
|
+
"""Raise if a tool with the same name already exists."""
|
|
72
|
+
try:
|
|
73
|
+
existing = client.find_tools(name=tool_name)
|
|
74
|
+
if existing:
|
|
75
|
+
raise click.ClickException(
|
|
76
|
+
f"A tool named '{tool_name}' already exists. "
|
|
77
|
+
"Please change your plugin's 'name' to a unique value, then re-run."
|
|
78
|
+
)
|
|
79
|
+
except click.ClickException:
|
|
80
|
+
# Re-raise ClickException (intended error)
|
|
81
|
+
raise
|
|
82
|
+
except Exception:
|
|
83
|
+
# Non-fatal: best-effort duplicate check for other errors
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_tags(tags: str | None) -> list[str]:
|
|
88
|
+
return [t.strip() for t in (tags.split(",") if tags else []) if t.strip()]
|
|
53
89
|
|
|
54
90
|
|
|
55
91
|
@tools_group.command(name="list")
|
|
@@ -70,20 +106,10 @@ def list_tools(ctx):
|
|
|
70
106
|
|
|
71
107
|
# Transform function for safe dictionary access
|
|
72
108
|
def transform_tool(tool):
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"name": tool.get("name", "N/A"),
|
|
78
|
-
"framework": tool.get("framework", "N/A"),
|
|
79
|
-
}
|
|
80
|
-
else:
|
|
81
|
-
# Fallback to attribute access
|
|
82
|
-
return {
|
|
83
|
-
"id": str(getattr(tool, "id", "N/A")),
|
|
84
|
-
"name": getattr(tool, "name", "N/A"),
|
|
85
|
-
"framework": getattr(tool, "framework", "N/A"),
|
|
86
|
-
}
|
|
109
|
+
row = coerce_to_row(tool, ["id", "name", "framework"])
|
|
110
|
+
# Ensure id is always a string
|
|
111
|
+
row["id"] = str(row["id"])
|
|
112
|
+
return row
|
|
87
113
|
|
|
88
114
|
output_list(ctx, tools, "🔧 Available Tools", columns, transform_tool)
|
|
89
115
|
|
|
@@ -92,6 +118,7 @@ def list_tools(ctx):
|
|
|
92
118
|
|
|
93
119
|
|
|
94
120
|
@tools_group.command()
|
|
121
|
+
@click.argument("file_arg", required=False, type=click.Path(exists=True))
|
|
95
122
|
@click.option(
|
|
96
123
|
"--file",
|
|
97
124
|
type=click.Path(exists=True),
|
|
@@ -111,11 +138,15 @@ def list_tools(ctx):
|
|
|
111
138
|
)
|
|
112
139
|
@output_flags()
|
|
113
140
|
@click.pass_context
|
|
114
|
-
def create(ctx, file, name, description, tags):
|
|
141
|
+
def create(ctx, file_arg, file, name, description, tags):
|
|
115
142
|
"""Create a new tool."""
|
|
116
143
|
try:
|
|
117
144
|
client = get_client(ctx)
|
|
118
145
|
|
|
146
|
+
# Allow positional file argument for better DX (matches examples)
|
|
147
|
+
if not file and file_arg:
|
|
148
|
+
file = file_arg
|
|
149
|
+
|
|
119
150
|
# Validate required parameters based on creation method
|
|
120
151
|
if not file:
|
|
121
152
|
# Metadata-only tool creation
|
|
@@ -126,27 +157,33 @@ def create(ctx, file, name, description, tags):
|
|
|
126
157
|
|
|
127
158
|
# Create tool based on whether file is provided
|
|
128
159
|
if file:
|
|
129
|
-
# File-based tool creation
|
|
160
|
+
# File-based tool creation — validate internal plugin name, no rewriting
|
|
130
161
|
with open(file, encoding="utf-8") as f:
|
|
131
162
|
code_content = f.read()
|
|
132
163
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
internal_name = _extract_internal_name(code_content)
|
|
165
|
+
tool_name = _validate_name_match(name, internal_name)
|
|
166
|
+
_check_duplicate_name(client, tool_name)
|
|
167
|
+
|
|
168
|
+
# Upload the plugin code as-is (no rewrite)
|
|
169
|
+
tool = client.create_tool_from_code(
|
|
170
|
+
tool_name,
|
|
171
|
+
code_content,
|
|
172
|
+
framework="langchain", # Always langchain
|
|
173
|
+
description=description,
|
|
174
|
+
tags=_parse_tags(tags),
|
|
175
|
+
)
|
|
141
176
|
else:
|
|
142
177
|
# Metadata-only tool creation
|
|
143
178
|
tool_kwargs = {}
|
|
144
179
|
if name:
|
|
145
180
|
tool_kwargs["name"] = name
|
|
181
|
+
tool_kwargs["tool_type"] = "custom" # Always custom
|
|
182
|
+
tool_kwargs["framework"] = "langchain" # Always langchain
|
|
146
183
|
if description:
|
|
147
184
|
tool_kwargs["description"] = description
|
|
148
185
|
if tags:
|
|
149
|
-
tool_kwargs["tags"] =
|
|
186
|
+
tool_kwargs["tags"] = _parse_tags(tags)
|
|
150
187
|
|
|
151
188
|
tool = client.create_tool(**tool_kwargs)
|
|
152
189
|
|
|
@@ -160,8 +197,8 @@ def create(ctx, file, name, description, tags):
|
|
|
160
197
|
panel = Panel(
|
|
161
198
|
f"[green]✅ Tool '{tool.name}' created successfully via {creation_method}![/green]\n\n"
|
|
162
199
|
f"ID: {tool.id}\n"
|
|
163
|
-
f"Framework:
|
|
164
|
-
f"Type: {'
|
|
200
|
+
f"Framework: {getattr(tool, 'framework', 'N/A')} (default)\n"
|
|
201
|
+
f"Type: {getattr(tool, 'tool_type', 'N/A')} (auto-detected)\n"
|
|
165
202
|
f"Description: {getattr(tool, 'description', 'No description')}",
|
|
166
203
|
title="🔧 Tool Created",
|
|
167
204
|
border_style="green",
|
|
@@ -307,3 +344,102 @@ def delete(ctx, tool_id, yes):
|
|
|
307
344
|
else:
|
|
308
345
|
console.print(f"[red]Error deleting tool: {e}[/red]")
|
|
309
346
|
raise click.ClickException(str(e))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@tools_group.command()
|
|
350
|
+
@click.argument("tool_id")
|
|
351
|
+
@output_flags()
|
|
352
|
+
@click.pass_context
|
|
353
|
+
def script(ctx, tool_id):
|
|
354
|
+
"""Get tool script content."""
|
|
355
|
+
try:
|
|
356
|
+
client = get_client(ctx)
|
|
357
|
+
|
|
358
|
+
# Get tool by ID (no ambiguity handling needed)
|
|
359
|
+
try:
|
|
360
|
+
tool = client.get_tool_by_id(tool_id)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
raise click.ClickException(f"Tool with ID '{tool_id}' not found: {e}")
|
|
363
|
+
|
|
364
|
+
# Get tool script content
|
|
365
|
+
script_content = client.tools.get_tool_script(tool_id)
|
|
366
|
+
|
|
367
|
+
if ctx.obj.get("view") == "json":
|
|
368
|
+
click.echo(
|
|
369
|
+
json.dumps(
|
|
370
|
+
{
|
|
371
|
+
"tool_id": tool_id,
|
|
372
|
+
"tool_name": tool.name,
|
|
373
|
+
"script": script_content,
|
|
374
|
+
},
|
|
375
|
+
indent=2,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
elif ctx.obj.get("output"):
|
|
379
|
+
# Save to file
|
|
380
|
+
output_file = ctx.obj.get("output")
|
|
381
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
382
|
+
f.write(script_content)
|
|
383
|
+
console.print(f"[green]✅ Tool script saved to {output_file}[/green]")
|
|
384
|
+
else:
|
|
385
|
+
# Display in terminal
|
|
386
|
+
console.print(
|
|
387
|
+
Panel(
|
|
388
|
+
script_content,
|
|
389
|
+
title=f"🔧 Tool Script: {tool.name}",
|
|
390
|
+
border_style="cyan",
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
if ctx.obj.get("view") == "json":
|
|
396
|
+
click.echo(json.dumps({"error": str(e)}, indent=2))
|
|
397
|
+
else:
|
|
398
|
+
console.print(f"[red]Error getting tool script: {e}[/red]")
|
|
399
|
+
raise click.ClickException(str(e))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@tools_group.command()
|
|
403
|
+
@click.argument("tool_id")
|
|
404
|
+
@click.option(
|
|
405
|
+
"--file",
|
|
406
|
+
type=click.Path(exists=True),
|
|
407
|
+
required=True,
|
|
408
|
+
help="New tool file for code update",
|
|
409
|
+
)
|
|
410
|
+
@click.option("--name", help="New tool name")
|
|
411
|
+
@click.option("--description", help="New description")
|
|
412
|
+
@click.option("--tags", help="Comma-separated tags")
|
|
413
|
+
@output_flags()
|
|
414
|
+
@click.pass_context
|
|
415
|
+
def upload_update(ctx, tool_id, file, name, description, tags):
|
|
416
|
+
"""Update a tool plugin via file upload."""
|
|
417
|
+
try:
|
|
418
|
+
client = get_client(ctx)
|
|
419
|
+
|
|
420
|
+
# Prepare update data
|
|
421
|
+
update_data = {}
|
|
422
|
+
if name:
|
|
423
|
+
update_data["name"] = name
|
|
424
|
+
if description:
|
|
425
|
+
update_data["description"] = description
|
|
426
|
+
if tags:
|
|
427
|
+
update_data["tags"] = [tag.strip() for tag in tags.split(",")]
|
|
428
|
+
|
|
429
|
+
# Update tool via file upload
|
|
430
|
+
updated_tool = client.tools.update_tool_via_file(tool_id, file, **update_data)
|
|
431
|
+
|
|
432
|
+
if ctx.obj.get("view") == "json":
|
|
433
|
+
click.echo(json.dumps(updated_tool.model_dump(), indent=2))
|
|
434
|
+
else:
|
|
435
|
+
console.print(
|
|
436
|
+
f"[green]✅ Tool '{updated_tool.name}' updated successfully via file upload[/green]"
|
|
437
|
+
)
|
|
438
|
+
console.print(f"[blue]📁 File: {file}[/blue]")
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
if ctx.obj.get("view") == "json":
|
|
442
|
+
click.echo(json.dumps({"error": str(e)}, indent=2))
|
|
443
|
+
else:
|
|
444
|
+
console.print(f"[red]Error updating tool: {e}[/red]")
|
|
445
|
+
raise click.ClickException(str(e))
|
glaip_sdk/cli/main.py
CHANGED
|
@@ -4,20 +4,32 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
|
|
9
11
|
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
10
15
|
|
|
16
|
+
from glaip_sdk import Client
|
|
17
|
+
from glaip_sdk._version import __version__ as _SDK_VERSION
|
|
11
18
|
from glaip_sdk.cli.commands.agents import agents_group
|
|
12
|
-
from glaip_sdk.cli.commands.configure import
|
|
19
|
+
from glaip_sdk.cli.commands.configure import (
|
|
20
|
+
config_group,
|
|
21
|
+
configure_command,
|
|
22
|
+
load_config,
|
|
23
|
+
)
|
|
13
24
|
from glaip_sdk.cli.commands.init import init_command
|
|
14
25
|
from glaip_sdk.cli.commands.mcps import mcps_group
|
|
15
26
|
from glaip_sdk.cli.commands.models import models_group
|
|
16
27
|
from glaip_sdk.cli.commands.tools import tools_group
|
|
28
|
+
from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
|
|
17
29
|
|
|
18
30
|
|
|
19
31
|
@click.group()
|
|
20
|
-
@click.version_option(version=
|
|
32
|
+
@click.version_option(version=_SDK_VERSION, prog_name="aip")
|
|
21
33
|
@click.option("--api-url", envvar="AIP_API_URL", help="AIP API URL")
|
|
22
34
|
@click.option("--api-key", envvar="AIP_API_KEY", help="AIP API Key")
|
|
23
35
|
@click.option("--timeout", default=30.0, help="Request timeout in seconds")
|
|
@@ -75,13 +87,6 @@ def status(ctx):
|
|
|
75
87
|
"""Show connection status and basic info."""
|
|
76
88
|
config = {}
|
|
77
89
|
try:
|
|
78
|
-
from rich.console import Console
|
|
79
|
-
from rich.panel import Panel
|
|
80
|
-
from rich.table import Table
|
|
81
|
-
|
|
82
|
-
from glaip_sdk import Client
|
|
83
|
-
from glaip_sdk.cli.commands.configure import load_config
|
|
84
|
-
|
|
85
90
|
console = Console()
|
|
86
91
|
|
|
87
92
|
# Load config from file and merge with context
|
|
@@ -89,7 +94,6 @@ def status(ctx):
|
|
|
89
94
|
context_config = ctx.obj or {}
|
|
90
95
|
|
|
91
96
|
# Load environment variables (middle priority)
|
|
92
|
-
import os
|
|
93
97
|
|
|
94
98
|
env_config = {}
|
|
95
99
|
if os.getenv("AIP_API_URL"):
|
|
@@ -146,7 +150,7 @@ def status(ctx):
|
|
|
146
150
|
Panel(
|
|
147
151
|
f"[bold green]✅ Connected to AIP Platform[/bold green]\n"
|
|
148
152
|
f"🔗 API URL: {client.api_url}\n"
|
|
149
|
-
f"
|
|
153
|
+
f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
|
|
150
154
|
title="🚀 Connection Status",
|
|
151
155
|
border_style="green",
|
|
152
156
|
)
|
|
@@ -191,9 +195,7 @@ def status(ctx):
|
|
|
191
195
|
@main.command()
|
|
192
196
|
def version():
|
|
193
197
|
"""Show version information."""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
click.echo(f"aip version {SDK_VERSION}")
|
|
198
|
+
click.echo(f"aip version {_SDK_VERSION}")
|
|
197
199
|
|
|
198
200
|
|
|
199
201
|
@main.command()
|
|
@@ -201,17 +203,13 @@ def version():
|
|
|
201
203
|
"--check-only", is_flag=True, help="Only check for updates without installing"
|
|
202
204
|
)
|
|
203
205
|
@click.option(
|
|
204
|
-
"--force",
|
|
206
|
+
"--force",
|
|
207
|
+
is_flag=True,
|
|
208
|
+
help="Force reinstall even if already up-to-date (adds --force-reinstall)",
|
|
205
209
|
)
|
|
206
210
|
def update(check_only: bool, force: bool):
|
|
207
211
|
"""Update AIP SDK to the latest version from PyPI."""
|
|
208
212
|
try:
|
|
209
|
-
import subprocess
|
|
210
|
-
import sys
|
|
211
|
-
|
|
212
|
-
from rich.console import Console
|
|
213
|
-
from rich.panel import Panel
|
|
214
|
-
|
|
215
213
|
console = Console()
|
|
216
214
|
|
|
217
215
|
if check_only:
|
|
@@ -238,12 +236,17 @@ def update(check_only: bool, force: bool):
|
|
|
238
236
|
|
|
239
237
|
# Update using pip
|
|
240
238
|
try:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
239
|
+
cmd = [
|
|
240
|
+
sys.executable,
|
|
241
|
+
"-m",
|
|
242
|
+
"pip",
|
|
243
|
+
"install",
|
|
244
|
+
"--upgrade",
|
|
245
|
+
"glaip-sdk",
|
|
246
|
+
]
|
|
247
|
+
if force:
|
|
248
|
+
cmd.insert(5, "--force-reinstall")
|
|
249
|
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
247
250
|
|
|
248
251
|
console.print(
|
|
249
252
|
Panel(
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
19
19
|
import click
|
|
20
20
|
from rich import box
|
|
21
21
|
from rich.console import Console, Group
|
|
22
|
+
from rich.markdown import Markdown
|
|
22
23
|
from rich.panel import Panel
|
|
23
24
|
from rich.pretty import Pretty
|
|
24
25
|
from rich.table import Table
|
|
@@ -41,6 +42,15 @@ except Exception:
|
|
|
41
42
|
if TYPE_CHECKING:
|
|
42
43
|
from glaip_sdk import Client
|
|
43
44
|
|
|
45
|
+
from glaip_sdk import Client
|
|
46
|
+
from glaip_sdk.cli.commands.configure import load_config
|
|
47
|
+
from glaip_sdk.utils import is_uuid
|
|
48
|
+
from glaip_sdk.utils.rendering.renderer import (
|
|
49
|
+
CapturingConsole,
|
|
50
|
+
RendererConfig,
|
|
51
|
+
RichStreamRenderer,
|
|
52
|
+
)
|
|
53
|
+
|
|
44
54
|
console = Console()
|
|
45
55
|
|
|
46
56
|
|
|
@@ -160,9 +170,6 @@ def _get_view(ctx) -> str:
|
|
|
160
170
|
|
|
161
171
|
def get_client(ctx) -> Client:
|
|
162
172
|
"""Get configured client from context, env, and config file (ctx > env > file)."""
|
|
163
|
-
from glaip_sdk import Client
|
|
164
|
-
from glaip_sdk.cli.commands.configure import load_config
|
|
165
|
-
|
|
166
173
|
file_config = load_config() or {}
|
|
167
174
|
context_config = (ctx.obj or {}) if ctx else {}
|
|
168
175
|
|
|
@@ -481,8 +488,6 @@ def output_result(
|
|
|
481
488
|
|
|
482
489
|
if fmt == "md":
|
|
483
490
|
try:
|
|
484
|
-
from rich.markdown import Markdown
|
|
485
|
-
|
|
486
491
|
console.print(Markdown(str(data)))
|
|
487
492
|
except ImportError:
|
|
488
493
|
# Fallback to plain if markdown not available
|
|
@@ -532,6 +537,14 @@ def output_list(
|
|
|
532
537
|
except Exception:
|
|
533
538
|
rows = []
|
|
534
539
|
|
|
540
|
+
# Mask secrets (apply before any view)
|
|
541
|
+
mask_fields = _resolve_mask_fields()
|
|
542
|
+
if mask_fields:
|
|
543
|
+
try:
|
|
544
|
+
rows = [_maybe_mask_row(r, mask_fields) for r in rows]
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
|
|
535
548
|
# JSON view bypasses any UI
|
|
536
549
|
if fmt == "json":
|
|
537
550
|
data = rows or [it.to_dict() if hasattr(it, "to_dict") else it for it in items]
|
|
@@ -578,14 +591,6 @@ def output_list(
|
|
|
578
591
|
except Exception:
|
|
579
592
|
pass
|
|
580
593
|
|
|
581
|
-
# Mask secrets
|
|
582
|
-
mask_fields = _resolve_mask_fields()
|
|
583
|
-
if mask_fields:
|
|
584
|
-
try:
|
|
585
|
-
rows = [_maybe_mask_row(r, mask_fields) for r in rows]
|
|
586
|
-
except Exception:
|
|
587
|
-
pass
|
|
588
|
-
|
|
589
594
|
# === Fuzzy palette is the default for TTY lists ===
|
|
590
595
|
picked: dict[str, Any] | None = None
|
|
591
596
|
if console.is_terminal and os.isatty(1):
|
|
@@ -685,6 +690,114 @@ def output_flags():
|
|
|
685
690
|
# ------------------------- Ambiguity handling --------------------------- #
|
|
686
691
|
|
|
687
692
|
|
|
693
|
+
def coerce_to_row(item, keys: list[str]) -> dict[str, Any]:
|
|
694
|
+
"""Coerce an item (dict or object) to a row dict with specified keys.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
item: The item to coerce (dict or object with attributes)
|
|
698
|
+
keys: List of keys/attribute names to extract
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Dict with the extracted values, "N/A" for missing values
|
|
702
|
+
"""
|
|
703
|
+
result = {}
|
|
704
|
+
for key in keys:
|
|
705
|
+
if isinstance(item, dict):
|
|
706
|
+
value = item.get(key, "N/A")
|
|
707
|
+
else:
|
|
708
|
+
value = getattr(item, key, "N/A")
|
|
709
|
+
result[key] = str(value) if value is not None else "N/A"
|
|
710
|
+
return result
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def build_renderer(
|
|
714
|
+
ctx,
|
|
715
|
+
*,
|
|
716
|
+
save_path,
|
|
717
|
+
theme="dark",
|
|
718
|
+
verbose=False,
|
|
719
|
+
tty_enabled=True,
|
|
720
|
+
live=None,
|
|
721
|
+
snapshots=None,
|
|
722
|
+
):
|
|
723
|
+
"""Build renderer and capturing console for CLI commands.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
ctx: Click context
|
|
727
|
+
save_path: Path to save output to (enables capturing)
|
|
728
|
+
theme: Color theme ("dark" or "light")
|
|
729
|
+
verbose: Whether to enable verbose mode
|
|
730
|
+
tty_enabled: Whether TTY is available
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Tuple of (renderer, capturing_console)
|
|
734
|
+
"""
|
|
735
|
+
# Use capturing console if saving output
|
|
736
|
+
working_console = console
|
|
737
|
+
if save_path:
|
|
738
|
+
working_console = CapturingConsole(console, capture=True)
|
|
739
|
+
|
|
740
|
+
# Decide live behavior: default is live unless verbose; allow explicit override
|
|
741
|
+
live_enabled = (not verbose) if live is None else bool(live)
|
|
742
|
+
renderer_cfg = RendererConfig(
|
|
743
|
+
theme=theme,
|
|
744
|
+
style="debug" if verbose else "pretty",
|
|
745
|
+
live=live_enabled,
|
|
746
|
+
show_delegate_tool_panels=True,
|
|
747
|
+
append_finished_snapshots=bool(snapshots)
|
|
748
|
+
if snapshots is not None
|
|
749
|
+
else RendererConfig.append_finished_snapshots,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Create the renderer instance
|
|
753
|
+
renderer = RichStreamRenderer(
|
|
754
|
+
working_console.original_console
|
|
755
|
+
if isinstance(working_console, CapturingConsole)
|
|
756
|
+
else working_console,
|
|
757
|
+
cfg=renderer_cfg,
|
|
758
|
+
verbose=verbose,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
return renderer, working_console
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def resolve_resource(
|
|
765
|
+
ctx, ref: str, *, get_by_id, find_by_name, label: str, select: int | None = None
|
|
766
|
+
):
|
|
767
|
+
"""Resolve resource reference (ID or name) with ambiguity handling.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
ctx: Click context
|
|
771
|
+
ref: Resource reference (ID or name)
|
|
772
|
+
get_by_id: Function to get resource by ID
|
|
773
|
+
find_by_name: Function to find resources by name
|
|
774
|
+
label: Resource type label for error messages
|
|
775
|
+
select: Optional selection index for ambiguity resolution
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
Resolved resource object
|
|
779
|
+
"""
|
|
780
|
+
if is_uuid(ref):
|
|
781
|
+
return get_by_id(ref)
|
|
782
|
+
|
|
783
|
+
# Find resources by name
|
|
784
|
+
matches = find_by_name(name=ref)
|
|
785
|
+
if not matches:
|
|
786
|
+
raise click.ClickException(f"{label} '{ref}' not found")
|
|
787
|
+
|
|
788
|
+
if len(matches) == 1:
|
|
789
|
+
return matches[0]
|
|
790
|
+
|
|
791
|
+
# Multiple matches - handle ambiguity
|
|
792
|
+
if select:
|
|
793
|
+
idx = int(select) - 1
|
|
794
|
+
if not (0 <= idx < len(matches)):
|
|
795
|
+
raise click.ClickException(f"--select must be 1..{len(matches)}")
|
|
796
|
+
return matches[idx]
|
|
797
|
+
|
|
798
|
+
return handle_ambiguous_resource(ctx, label.lower(), ref, matches)
|
|
799
|
+
|
|
800
|
+
|
|
688
801
|
def handle_ambiguous_resource(
|
|
689
802
|
ctx, resource_type: str, ref: str, matches: list[Any]
|
|
690
803
|
) -> Any:
|