glaip-sdk 0.0.5b1__py3-none-any.whl → 0.0.6a0__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 (41) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/branding.py +3 -2
  3. glaip_sdk/cli/commands/__init__.py +1 -1
  4. glaip_sdk/cli/commands/agents.py +444 -268
  5. glaip_sdk/cli/commands/configure.py +12 -11
  6. glaip_sdk/cli/commands/mcps.py +28 -16
  7. glaip_sdk/cli/commands/models.py +5 -3
  8. glaip_sdk/cli/commands/tools.py +109 -102
  9. glaip_sdk/cli/display.py +38 -16
  10. glaip_sdk/cli/io.py +1 -1
  11. glaip_sdk/cli/main.py +26 -5
  12. glaip_sdk/cli/resolution.py +5 -4
  13. glaip_sdk/cli/utils.py +376 -157
  14. glaip_sdk/cli/validators.py +7 -2
  15. glaip_sdk/client/agents.py +184 -89
  16. glaip_sdk/client/base.py +24 -13
  17. glaip_sdk/client/validators.py +154 -94
  18. glaip_sdk/config/constants.py +0 -2
  19. glaip_sdk/models.py +4 -4
  20. glaip_sdk/utils/__init__.py +7 -7
  21. glaip_sdk/utils/client_utils.py +144 -78
  22. glaip_sdk/utils/display.py +4 -2
  23. glaip_sdk/utils/general.py +8 -6
  24. glaip_sdk/utils/import_export.py +55 -24
  25. glaip_sdk/utils/rendering/formatting.py +12 -6
  26. glaip_sdk/utils/rendering/models.py +1 -1
  27. glaip_sdk/utils/rendering/renderer/base.py +412 -248
  28. glaip_sdk/utils/rendering/renderer/console.py +6 -5
  29. glaip_sdk/utils/rendering/renderer/debug.py +94 -52
  30. glaip_sdk/utils/rendering/renderer/stream.py +93 -48
  31. glaip_sdk/utils/rendering/steps.py +103 -39
  32. glaip_sdk/utils/rich_utils.py +1 -1
  33. glaip_sdk/utils/run_renderer.py +1 -1
  34. glaip_sdk/utils/serialization.py +3 -1
  35. glaip_sdk/utils/validation.py +2 -2
  36. glaip_sdk-0.0.6a0.dist-info/METADATA +183 -0
  37. glaip_sdk-0.0.6a0.dist-info/RECORD +55 -0
  38. glaip_sdk-0.0.5b1.dist-info/METADATA +0 -645
  39. glaip_sdk-0.0.5b1.dist-info/RECORD +0 -55
  40. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.6a0.dist-info}/WHEEL +0 -0
  41. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.6a0.dist-info}/entry_points.txt +0 -0
@@ -7,6 +7,7 @@ Authors:
7
7
  import getpass
8
8
  import os
9
9
  from pathlib import Path
10
+ from typing import Any
10
11
 
11
12
  import click
12
13
  import yaml
@@ -24,7 +25,7 @@ CONFIG_DIR = Path.home() / ".aip"
24
25
  CONFIG_FILE = CONFIG_DIR / "config.yaml"
25
26
 
26
27
 
27
- def load_config():
28
+ def load_config() -> dict[str, Any]:
28
29
  """Load configuration from file."""
29
30
  if not CONFIG_FILE.exists():
30
31
  return {}
@@ -36,7 +37,7 @@ def load_config():
36
37
  return {}
37
38
 
38
39
 
39
- def save_config(config):
40
+ def save_config(config: dict[str, Any]) -> None:
40
41
  """Save configuration to file."""
41
42
  CONFIG_DIR.mkdir(exist_ok=True)
42
43
 
@@ -51,13 +52,13 @@ def save_config(config):
51
52
 
52
53
 
53
54
  @click.group()
54
- def config_group():
55
+ def config_group() -> None:
55
56
  """Configuration management operations."""
56
57
  pass
57
58
 
58
59
 
59
60
  @config_group.command("list")
60
- def list_config():
61
+ def list_config() -> None:
61
62
  """List current configuration."""
62
63
 
63
64
  config = load_config()
@@ -87,7 +88,7 @@ def list_config():
87
88
  @config_group.command("set")
88
89
  @click.argument("key")
89
90
  @click.argument("value")
90
- def set_config(key, value):
91
+ def set_config(key: str, value: str) -> None:
91
92
  """Set a configuration value."""
92
93
 
93
94
  valid_keys = ["api_url", "api_key"]
@@ -111,7 +112,7 @@ def set_config(key, value):
111
112
 
112
113
  @config_group.command("get")
113
114
  @click.argument("key")
114
- def get_config(key):
115
+ def get_config(key: str) -> None:
115
116
  """Get a configuration value."""
116
117
 
117
118
  config = load_config()
@@ -132,7 +133,7 @@ def get_config(key):
132
133
 
133
134
  @config_group.command("unset")
134
135
  @click.argument("key")
135
- def unset_config(key):
136
+ def unset_config(key: str) -> None:
136
137
  """Remove a configuration value."""
137
138
 
138
139
  config = load_config()
@@ -149,7 +150,7 @@ def unset_config(key):
149
150
 
150
151
  @config_group.command("reset")
151
152
  @click.option("--force", is_flag=True, help="Skip confirmation prompt")
152
- def reset_config(force):
153
+ def reset_config(force: bool) -> None:
153
154
  """Reset all configuration to defaults."""
154
155
 
155
156
  if not force:
@@ -168,7 +169,7 @@ def reset_config(force):
168
169
  console.print("[yellow]No configuration found to reset.[/yellow]")
169
170
 
170
171
 
171
- def _configure_interactive():
172
+ def _configure_interactive() -> None:
172
173
  """Shared configuration logic for both configure commands."""
173
174
  # Display AIP welcome banner
174
175
  branding = AIPBranding.create_from_sdk(
@@ -239,14 +240,14 @@ def _configure_interactive():
239
240
 
240
241
 
241
242
  @config_group.command()
242
- def configure():
243
+ def configure() -> None:
243
244
  """Configure AIP CLI credentials and settings interactively."""
244
245
  _configure_interactive()
245
246
 
246
247
 
247
248
  # Alias command for backward compatibility
248
249
  @click.command()
249
- def configure_command():
250
+ def configure_command() -> None:
250
251
  """Configure AIP CLI credentials and settings interactively.
251
252
 
252
253
  This is an alias for 'aip config configure' for backward compatibility.
@@ -6,6 +6,7 @@ Authors:
6
6
 
7
7
  import json
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
 
10
11
  import click
11
12
  from rich.console import Console
@@ -30,6 +31,7 @@ from glaip_sdk.cli.resolution import resolve_resource_reference
30
31
  from glaip_sdk.cli.utils import (
31
32
  coerce_to_row,
32
33
  get_client,
34
+ get_ctx_value,
33
35
  output_flags,
34
36
  output_list,
35
37
  output_result,
@@ -41,12 +43,14 @@ console = Console()
41
43
 
42
44
 
43
45
  @click.group(name="mcps", no_args_is_help=True)
44
- def mcps_group():
46
+ def mcps_group() -> None:
45
47
  """MCP management operations."""
46
48
  pass
47
49
 
48
50
 
49
- def _resolve_mcp(ctx, client, ref, select=None):
51
+ def _resolve_mcp(
52
+ ctx: Any, client: Any, ref: str, select: int | None = None
53
+ ) -> Any | None:
50
54
  """Resolve MCP reference (ID or name) with ambiguity handling."""
51
55
  return resolve_resource_reference(
52
56
  ctx,
@@ -63,7 +67,7 @@ def _resolve_mcp(ctx, client, ref, select=None):
63
67
  @mcps_group.command(name="list")
64
68
  @output_flags()
65
69
  @click.pass_context
66
- def list_mcps(ctx):
70
+ def list_mcps(ctx: Any) -> None:
67
71
  """List all MCPs."""
68
72
  try:
69
73
  client = get_client(ctx)
@@ -77,7 +81,7 @@ def list_mcps(ctx):
77
81
  ]
78
82
 
79
83
  # Transform function for safe dictionary access
80
- def transform_mcp(mcp):
84
+ def transform_mcp(mcp: Any) -> dict[str, Any]:
81
85
  row = coerce_to_row(mcp, ["id", "name", "config"])
82
86
  # Ensure id is always a string
83
87
  row["id"] = str(row["id"])
@@ -103,7 +107,9 @@ def list_mcps(ctx):
103
107
  @click.option("--config", help="JSON configuration string")
104
108
  @output_flags()
105
109
  @click.pass_context
106
- def create(ctx, name, transport, description, config):
110
+ def create(
111
+ ctx: Any, name: str, transport: str, description: str | None, config: str | None
112
+ ) -> None:
107
113
  """Create a new MCP."""
108
114
  try:
109
115
  client = get_client(ctx)
@@ -140,7 +146,7 @@ def create(ctx, name, transport, description, config):
140
146
 
141
147
  except Exception as e:
142
148
  handle_json_output(ctx, error=e)
143
- if ctx.obj.get("view") != "json":
149
+ if get_ctx_value(ctx, "view") != "json":
144
150
  display_api_error(e, "MCP creation")
145
151
  raise click.ClickException(str(e))
146
152
 
@@ -154,7 +160,7 @@ def create(ctx, name, transport, description, config):
154
160
  )
155
161
  @output_flags()
156
162
  @click.pass_context
157
- def get(ctx, mcp_ref, export):
163
+ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
158
164
  """Get MCP details.
159
165
 
160
166
  Examples:
@@ -244,7 +250,7 @@ def get(ctx, mcp_ref, export):
244
250
  @click.argument("mcp_ref")
245
251
  @output_flags()
246
252
  @click.pass_context
247
- def list_tools(ctx, mcp_ref):
253
+ def list_tools(ctx: Any, mcp_ref: str) -> None:
248
254
  """List tools from MCP."""
249
255
  try:
250
256
  client = get_client(ctx)
@@ -263,7 +269,7 @@ def list_tools(ctx, mcp_ref):
263
269
  ]
264
270
 
265
271
  # Transform function for safe dictionary access
266
- def transform_tool(tool):
272
+ def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
267
273
  return {
268
274
  "name": tool.get("name", "N/A"),
269
275
  "description": tool.get("description", "N/A")[:47] + "..."
@@ -289,7 +295,7 @@ def list_tools(ctx, mcp_ref):
289
295
  )
290
296
  @output_flags()
291
297
  @click.pass_context
292
- def connect(ctx, config_file):
298
+ def connect(ctx: Any, config_file: str) -> None:
293
299
  """Connect to MCP using config file."""
294
300
  try:
295
301
  client = get_client(ctx)
@@ -298,7 +304,7 @@ def connect(ctx, config_file):
298
304
  with open(config_file) as f:
299
305
  config = json.load(f)
300
306
 
301
- view = (ctx.obj or {}).get("view", "rich")
307
+ view = get_ctx_value(ctx, "view", "rich")
302
308
  if view != "json":
303
309
  console.print(
304
310
  Text(
@@ -309,7 +315,7 @@ def connect(ctx, config_file):
309
315
  # Test connection using config
310
316
  result = client.mcps.test_mcp_connection_from_config(config)
311
317
 
312
- view = (ctx.obj or {}).get("view", "rich")
318
+ view = get_ctx_value(ctx, "view", "rich")
313
319
  if view == "json":
314
320
  handle_json_output(ctx, result)
315
321
  else:
@@ -332,7 +338,13 @@ def connect(ctx, config_file):
332
338
  @click.option("--config", help="JSON configuration string")
333
339
  @output_flags()
334
340
  @click.pass_context
335
- def update(ctx, mcp_ref, name, description, config):
341
+ def update(
342
+ ctx: Any,
343
+ mcp_ref: str,
344
+ name: str | None,
345
+ description: str | None,
346
+ config: str | None,
347
+ ) -> None:
336
348
  """Update an existing MCP."""
337
349
  try:
338
350
  client = get_client(ctx)
@@ -363,7 +375,7 @@ def update(ctx, mcp_ref, name, description, config):
363
375
 
364
376
  except Exception as e:
365
377
  handle_json_output(ctx, error=e)
366
- if (ctx.obj or {}).get("view") != "json":
378
+ if get_ctx_value(ctx, "view") != "json":
367
379
  display_api_error(e, "MCP update")
368
380
  raise click.ClickException(str(e))
369
381
 
@@ -373,7 +385,7 @@ def update(ctx, mcp_ref, name, description, config):
373
385
  @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
374
386
  @output_flags()
375
387
  @click.pass_context
376
- def delete(ctx, mcp_ref, yes):
388
+ def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
377
389
  """Delete an MCP."""
378
390
  try:
379
391
  client = get_client(ctx)
@@ -398,6 +410,6 @@ def delete(ctx, mcp_ref, yes):
398
410
 
399
411
  except Exception as e:
400
412
  handle_json_output(ctx, error=e)
401
- if (ctx.obj or {}).get("view") != "json":
413
+ if get_ctx_value(ctx, "view") != "json":
402
414
  display_api_error(e, "MCP deletion")
403
415
  raise click.ClickException(str(e))
@@ -4,6 +4,8 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
+ from typing import Any
8
+
7
9
  import click
8
10
  from rich.console import Console
9
11
 
@@ -13,7 +15,7 @@ console = Console()
13
15
 
14
16
 
15
17
  @click.group(name="models", no_args_is_help=True)
16
- def models_group():
18
+ def models_group() -> None:
17
19
  """Language model operations."""
18
20
  pass
19
21
 
@@ -21,7 +23,7 @@ def models_group():
21
23
  @models_group.command(name="list")
22
24
  @output_flags()
23
25
  @click.pass_context
24
- def list_models(ctx):
26
+ def list_models(ctx: Any) -> None:
25
27
  """List available language models."""
26
28
  try:
27
29
  client = get_client(ctx)
@@ -36,7 +38,7 @@ def list_models(ctx):
36
38
  ]
37
39
 
38
40
  # Transform function for safe dictionary access
39
- def transform_model(model):
41
+ def transform_model(model: dict[str, Any]) -> dict[str, Any]:
40
42
  return {
41
43
  "id": str(model.get("id", "N/A")),
42
44
  "provider": model.get("provider", "N/A"),
@@ -7,6 +7,7 @@ Authors:
7
7
  import json
8
8
  import re
9
9
  from pathlib import Path
10
+ from typing import Any
10
11
 
11
12
  import click
12
13
  from rich.console import Console
@@ -34,6 +35,7 @@ from glaip_sdk.cli.resolution import resolve_resource_reference
34
35
  from glaip_sdk.cli.utils import (
35
36
  coerce_to_row,
36
37
  get_client,
38
+ get_ctx_value,
37
39
  output_flags,
38
40
  output_list,
39
41
  output_result,
@@ -45,12 +47,14 @@ console = Console()
45
47
 
46
48
 
47
49
  @click.group(name="tools", no_args_is_help=True)
48
- def tools_group():
50
+ def tools_group() -> None:
49
51
  """Tool management operations."""
50
52
  pass
51
53
 
52
54
 
53
- def _resolve_tool(ctx, client, ref, select=None):
55
+ def _resolve_tool(
56
+ ctx: Any, client: Any, ref: str, select: int | None = None
57
+ ) -> Any | None:
54
58
  """Resolve tool reference (ID or name) with ambiguity handling."""
55
59
  return resolve_resource_reference(
56
60
  ctx,
@@ -90,7 +94,7 @@ def _validate_name_match(provided: str | None, internal: str) -> str:
90
94
  return provided or internal
91
95
 
92
96
 
93
- def _check_duplicate_name(client, tool_name: str) -> None:
97
+ def _check_duplicate_name(client: Any, tool_name: str) -> None:
94
98
  """Raise if a tool with the same name already exists."""
95
99
  try:
96
100
  existing = client.find_tools(name=tool_name)
@@ -111,6 +115,69 @@ def _parse_tags(tags: str | None) -> list[str]:
111
115
  return [t.strip() for t in (tags.split(",") if tags else []) if t.strip()]
112
116
 
113
117
 
118
+ def _handle_import_file(
119
+ import_file: str | None,
120
+ name: str | None,
121
+ description: str | None,
122
+ tags: tuple[str, ...] | None,
123
+ ) -> dict[str, Any]:
124
+ """Handle import file logic and merge with CLI arguments."""
125
+ if import_file:
126
+ import_data = load_resource_from_file(Path(import_file), "tool")
127
+
128
+ # Merge CLI args with imported data
129
+ cli_args = {
130
+ "name": name,
131
+ "description": description,
132
+ "tags": tags,
133
+ }
134
+
135
+ return merge_import_with_cli_args(import_data, cli_args)
136
+ else:
137
+ # No import file - use CLI args directly
138
+ return {
139
+ "name": name,
140
+ "description": description,
141
+ "tags": tags,
142
+ }
143
+
144
+
145
+ def _create_tool_from_file(
146
+ client: Any,
147
+ file_path: str,
148
+ name: str | None,
149
+ description: str | None,
150
+ tags: str | None,
151
+ ) -> Any:
152
+ """Create tool from file upload."""
153
+ with open(file_path, encoding="utf-8") as f:
154
+ code_content = f.read()
155
+
156
+ internal_name = _extract_internal_name(code_content)
157
+ tool_name = _validate_name_match(name, internal_name)
158
+ _check_duplicate_name(client, tool_name)
159
+
160
+ # Upload the plugin code as-is (no rewrite)
161
+ return client.create_tool_from_code(
162
+ name=tool_name,
163
+ code=code_content,
164
+ framework="langchain", # Always langchain
165
+ description=description,
166
+ tags=_parse_tags(tags) if tags else None,
167
+ )
168
+
169
+
170
+ def _validate_creation_parameters(
171
+ file: str | None,
172
+ import_file: str | None,
173
+ ) -> None:
174
+ """Validate required parameters for tool creation."""
175
+ if not file and not import_file:
176
+ raise click.ClickException(
177
+ "A tool file must be provided. Use --file to specify the tool file to upload."
178
+ )
179
+
180
+
114
181
  @tools_group.command(name="list")
115
182
  @output_flags()
116
183
  @click.option(
@@ -121,7 +188,7 @@ def _parse_tags(tags: str | None) -> list[str]:
121
188
  required=False,
122
189
  )
123
190
  @click.pass_context
124
- def list_tools(ctx, tool_type):
191
+ def list_tools(ctx: Any, tool_type: str | None) -> None:
125
192
  """List all tools."""
126
193
  try:
127
194
  client = get_client(ctx)
@@ -135,7 +202,7 @@ def list_tools(ctx, tool_type):
135
202
  ]
136
203
 
137
204
  # Transform function for safe dictionary access
138
- def transform_tool(tool):
205
+ def transform_tool(tool: Any) -> dict[str, Any]:
139
206
  row = coerce_to_row(tool, ["id", "name", "framework"])
140
207
  # Ensure id is always a string
141
208
  row["id"] = str(row["id"])
@@ -152,15 +219,15 @@ def list_tools(ctx, tool_type):
152
219
  @click.option(
153
220
  "--file",
154
221
  type=click.Path(exists=True),
155
- help="Tool file to upload (optional for metadata-only tools)",
222
+ help="Tool file to upload",
156
223
  )
157
224
  @click.option(
158
225
  "--name",
159
- help="Tool name (required for metadata-only tools, extracted from script if file provided)",
226
+ help="Tool name (extracted from script if file provided)",
160
227
  )
161
228
  @click.option(
162
229
  "--description",
163
- help="Tool description (optional - extracted from script if file provided)",
230
+ help="Tool description (extracted from script if file provided)",
164
231
  )
165
232
  @click.option(
166
233
  "--tags",
@@ -174,113 +241,47 @@ def list_tools(ctx, tool_type):
174
241
  )
175
242
  @output_flags()
176
243
  @click.pass_context
177
- def create(ctx, file_arg, file, name, description, tags, import_file):
244
+ def create(
245
+ ctx: Any,
246
+ file_arg: str | None,
247
+ file: str | None,
248
+ name: str | None,
249
+ description: str | None,
250
+ tags: tuple[str, ...] | None,
251
+ import_file: str | None,
252
+ ) -> None:
178
253
  """Create a new tool.
179
254
 
180
255
  Examples:
181
- aip tools create --name "My Tool" --description "A helpful tool"
182
256
  aip tools create tool.py # Create from file
183
257
  aip tools create --import tool.json # Create from exported configuration
184
258
  """
185
259
  try:
186
260
  client = get_client(ctx)
187
261
 
188
- # Initialize merged_data for cases without import_file
189
- merged_data = {}
190
-
191
- # Handle import from file
192
- if import_file:
193
- import_data = load_resource_from_file(Path(import_file), "tool")
194
-
195
- # Merge CLI args with imported data
196
- cli_args = {
197
- "name": name,
198
- "description": description,
199
- "tags": tags,
200
- }
262
+ # Allow positional file argument for better DX (matches examples)
263
+ if not file and file_arg:
264
+ file = file_arg
201
265
 
202
- merged_data = merge_import_with_cli_args(import_data, cli_args)
203
- else:
204
- # No import file - use CLI args directly
205
- merged_data = {
206
- "name": name,
207
- "description": description,
208
- "tags": tags,
209
- }
266
+ # Handle import file and merge with CLI arguments
267
+ merged_data = _handle_import_file(import_file, name, description, tags)
210
268
 
211
269
  # Extract merged values
212
270
  name = merged_data.get("name")
213
271
  description = merged_data.get("description")
214
272
  tags = merged_data.get("tags")
215
273
 
216
- # Allow positional file argument for better DX (matches examples)
217
- if not file and file_arg:
218
- file = file_arg
219
-
220
- # Validate required parameters based on creation method
221
- if not file and not import_file:
222
- # Metadata-only tool creation
223
- if not name:
224
- raise click.ClickException(
225
- "--name is required when creating metadata-only tools"
226
- )
274
+ # Validate required parameters
275
+ _validate_creation_parameters(file, import_file)
227
276
 
228
- # Create tool based on whether file is provided
229
- if file:
230
- # File-based tool creation — validate internal plugin name, no rewriting
231
- with open(file, encoding="utf-8") as f:
232
- code_content = f.read()
233
-
234
- internal_name = _extract_internal_name(code_content)
235
- tool_name = _validate_name_match(name, internal_name)
236
- _check_duplicate_name(client, tool_name)
237
-
238
- # Upload the plugin code as-is (no rewrite)
239
- tool = client.create_tool_from_code(
240
- name=tool_name,
241
- code=code_content,
242
- framework="langchain", # Always langchain
243
- description=description,
244
- tags=_parse_tags(tags) if tags else None,
245
- )
246
- else:
247
- # Metadata-only tool creation or import from file
248
- tool_kwargs = {}
249
- if name:
250
- tool_kwargs["name"] = name
251
- tool_kwargs["tool_type"] = "custom" # Always custom
252
- tool_kwargs["framework"] = "langchain" # Always langchain
253
- if description:
254
- tool_kwargs["description"] = description
255
- if tags:
256
- tool_kwargs["tags"] = _parse_tags(tags)
257
-
258
- # If importing from file, include all other detected attributes
259
- if import_file:
260
- # Add all other attributes from import data (excluding already handled ones)
261
- excluded_fields = {
262
- "name",
263
- "description",
264
- "tags",
265
- # System-only fields that shouldn't be passed to create_tool
266
- "id",
267
- "created_at",
268
- "updated_at",
269
- "tool_type",
270
- "framework",
271
- "version",
272
- }
273
- for key, value in merged_data.items():
274
- if key not in excluded_fields and value is not None:
275
- tool_kwargs[key] = value
276
-
277
- tool = client.create_tool(**tool_kwargs)
277
+ # Create tool from file (either direct file or import file)
278
+ tool = _create_tool_from_file(client, file, name, description, tags)
278
279
 
279
280
  # Handle JSON output
280
281
  handle_json_output(ctx, tool.model_dump())
281
282
 
282
283
  # Handle Rich output
283
- creation_method = "file upload (custom)" if file else "metadata only (native)"
284
+ creation_method = "file upload (custom)"
284
285
  rich_panel = display_creation_success(
285
286
  "Tool",
286
287
  tool.name,
@@ -294,7 +295,7 @@ def create(ctx, file_arg, file, name, description, tags, import_file):
294
295
 
295
296
  except Exception as e:
296
297
  handle_json_output(ctx, error=e)
297
- if ctx.obj.get("view") != "json":
298
+ if get_ctx_value(ctx, "view") != "json":
298
299
  display_api_error(e, "tool creation")
299
300
  raise click.ClickException(str(e))
300
301
 
@@ -309,7 +310,7 @@ def create(ctx, file_arg, file, name, description, tags, import_file):
309
310
  )
310
311
  @output_flags()
311
312
  @click.pass_context
312
- def get(ctx, tool_ref, select, export):
313
+ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None:
313
314
  """Get tool details.
314
315
 
315
316
  Examples:
@@ -406,7 +407,13 @@ def get(ctx, tool_ref, select, export):
406
407
  @click.option("--tags", help="Comma-separated tags")
407
408
  @output_flags()
408
409
  @click.pass_context
409
- def update(ctx, tool_id, file, description, tags):
410
+ def update(
411
+ ctx: Any,
412
+ tool_id: str,
413
+ file: str | None,
414
+ description: str | None,
415
+ tags: tuple[str, ...] | None,
416
+ ) -> None:
410
417
  """Update a tool (code or metadata)."""
411
418
  try:
412
419
  client = get_client(ctx)
@@ -453,7 +460,7 @@ def update(ctx, tool_id, file, description, tags):
453
460
 
454
461
  except Exception as e:
455
462
  handle_json_output(ctx, error=e)
456
- if ctx.obj.get("view") != "json":
463
+ if get_ctx_value(ctx, "view") != "json":
457
464
  display_api_error(e, "tool update")
458
465
  raise click.ClickException(str(e))
459
466
 
@@ -463,7 +470,7 @@ def update(ctx, tool_id, file, description, tags):
463
470
  @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
464
471
  @output_flags()
465
472
  @click.pass_context
466
- def delete(ctx, tool_id, yes):
473
+ def delete(ctx: Any, tool_id: str, yes: bool) -> None:
467
474
  """Delete a tool."""
468
475
  try:
469
476
  client = get_client(ctx)
@@ -491,7 +498,7 @@ def delete(ctx, tool_id, yes):
491
498
 
492
499
  except Exception as e:
493
500
  handle_json_output(ctx, error=e)
494
- if ctx.obj.get("view") != "json":
501
+ if get_ctx_value(ctx, "view") != "json":
495
502
  display_api_error(e, "tool deletion")
496
503
  raise click.ClickException(str(e))
497
504
 
@@ -500,13 +507,13 @@ def delete(ctx, tool_id, yes):
500
507
  @click.argument("tool_id")
501
508
  @output_flags()
502
509
  @click.pass_context
503
- def script(ctx, tool_id):
510
+ def script(ctx: Any, tool_id: str) -> None:
504
511
  """Get tool script content."""
505
512
  try:
506
513
  client = get_client(ctx)
507
514
  script_content = client.get_tool_script(tool_id)
508
515
 
509
- if ctx.obj.get("view") == "json":
516
+ if get_ctx_value(ctx, "view") == "json":
510
517
  click.echo(json.dumps({"script": script_content}, indent=2))
511
518
  else:
512
519
  console.print(f"[green]📜 Tool Script for '{tool_id}':[/green]")
@@ -514,6 +521,6 @@ def script(ctx, tool_id):
514
521
 
515
522
  except Exception as e:
516
523
  handle_json_output(ctx, error=e)
517
- if ctx.obj.get("view") != "json":
524
+ if get_ctx_value(ctx, "view") != "json":
518
525
  console.print(Text(f"[red]Error getting tool script: {e}[/red]"))
519
526
  raise click.ClickException(str(e))