glaip-sdk 0.0.5b1__py3-none-any.whl → 0.0.7__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 (43) hide show
  1. glaip_sdk/__init__.py +1 -1
  2. glaip_sdk/_version.py +42 -19
  3. glaip_sdk/branding.py +3 -2
  4. glaip_sdk/cli/commands/__init__.py +1 -1
  5. glaip_sdk/cli/commands/agents.py +452 -285
  6. glaip_sdk/cli/commands/configure.py +14 -13
  7. glaip_sdk/cli/commands/mcps.py +30 -20
  8. glaip_sdk/cli/commands/models.py +5 -3
  9. glaip_sdk/cli/commands/tools.py +111 -106
  10. glaip_sdk/cli/display.py +48 -27
  11. glaip_sdk/cli/io.py +1 -1
  12. glaip_sdk/cli/main.py +26 -5
  13. glaip_sdk/cli/resolution.py +5 -4
  14. glaip_sdk/cli/utils.py +437 -188
  15. glaip_sdk/cli/validators.py +7 -2
  16. glaip_sdk/client/agents.py +276 -153
  17. glaip_sdk/client/base.py +69 -27
  18. glaip_sdk/client/tools.py +44 -26
  19. glaip_sdk/client/validators.py +154 -94
  20. glaip_sdk/config/constants.py +0 -2
  21. glaip_sdk/models.py +5 -4
  22. glaip_sdk/utils/__init__.py +7 -7
  23. glaip_sdk/utils/client_utils.py +191 -101
  24. glaip_sdk/utils/display.py +4 -2
  25. glaip_sdk/utils/general.py +8 -6
  26. glaip_sdk/utils/import_export.py +58 -25
  27. glaip_sdk/utils/rendering/formatting.py +12 -6
  28. glaip_sdk/utils/rendering/models.py +1 -1
  29. glaip_sdk/utils/rendering/renderer/base.py +523 -332
  30. glaip_sdk/utils/rendering/renderer/console.py +6 -5
  31. glaip_sdk/utils/rendering/renderer/debug.py +94 -52
  32. glaip_sdk/utils/rendering/renderer/stream.py +93 -48
  33. glaip_sdk/utils/rendering/steps.py +103 -39
  34. glaip_sdk/utils/rich_utils.py +1 -1
  35. glaip_sdk/utils/run_renderer.py +1 -1
  36. glaip_sdk/utils/serialization.py +9 -3
  37. glaip_sdk/utils/validation.py +2 -2
  38. glaip_sdk-0.0.7.dist-info/METADATA +183 -0
  39. glaip_sdk-0.0.7.dist-info/RECORD +55 -0
  40. glaip_sdk-0.0.5b1.dist-info/METADATA +0 -645
  41. glaip_sdk-0.0.5b1.dist-info/RECORD +0 -55
  42. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.7.dist-info}/WHEEL +0 -0
  43. {glaip_sdk-0.0.5b1.dist-info → glaip_sdk-0.0.7.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
 
@@ -46,18 +47,18 @@ def save_config(config):
46
47
  # Set secure file permissions
47
48
  try:
48
49
  os.chmod(CONFIG_FILE, 0o600)
49
- except Exception:
50
- pass # Best effort
50
+ except Exception: # pragma: no cover - platform dependent best effort
51
+ pass
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
@@ -29,7 +30,9 @@ from glaip_sdk.cli.io import (
29
30
  from glaip_sdk.cli.resolution import resolve_resource_reference
30
31
  from glaip_sdk.cli.utils import (
31
32
  coerce_to_row,
33
+ detect_export_format,
32
34
  get_client,
35
+ get_ctx_value,
33
36
  output_flags,
34
37
  output_list,
35
38
  output_result,
@@ -41,12 +44,14 @@ console = Console()
41
44
 
42
45
 
43
46
  @click.group(name="mcps", no_args_is_help=True)
44
- def mcps_group():
47
+ def mcps_group() -> None:
45
48
  """MCP management operations."""
46
49
  pass
47
50
 
48
51
 
49
- def _resolve_mcp(ctx, client, ref, select=None):
52
+ def _resolve_mcp(
53
+ ctx: Any, client: Any, ref: str, select: int | None = None
54
+ ) -> Any | None:
50
55
  """Resolve MCP reference (ID or name) with ambiguity handling."""
51
56
  return resolve_resource_reference(
52
57
  ctx,
@@ -63,7 +68,7 @@ def _resolve_mcp(ctx, client, ref, select=None):
63
68
  @mcps_group.command(name="list")
64
69
  @output_flags()
65
70
  @click.pass_context
66
- def list_mcps(ctx):
71
+ def list_mcps(ctx: Any) -> None:
67
72
  """List all MCPs."""
68
73
  try:
69
74
  client = get_client(ctx)
@@ -77,7 +82,7 @@ def list_mcps(ctx):
77
82
  ]
78
83
 
79
84
  # Transform function for safe dictionary access
80
- def transform_mcp(mcp):
85
+ def transform_mcp(mcp: Any) -> dict[str, Any]:
81
86
  row = coerce_to_row(mcp, ["id", "name", "config"])
82
87
  # Ensure id is always a string
83
88
  row["id"] = str(row["id"])
@@ -103,7 +108,9 @@ def list_mcps(ctx):
103
108
  @click.option("--config", help="JSON configuration string")
104
109
  @output_flags()
105
110
  @click.pass_context
106
- def create(ctx, name, transport, description, config):
111
+ def create(
112
+ ctx: Any, name: str, transport: str, description: str | None, config: str | None
113
+ ) -> None:
107
114
  """Create a new MCP."""
108
115
  try:
109
116
  client = get_client(ctx)
@@ -140,7 +147,7 @@ def create(ctx, name, transport, description, config):
140
147
 
141
148
  except Exception as e:
142
149
  handle_json_output(ctx, error=e)
143
- if ctx.obj.get("view") != "json":
150
+ if get_ctx_value(ctx, "view") != "json":
144
151
  display_api_error(e, "MCP creation")
145
152
  raise click.ClickException(str(e))
146
153
 
@@ -154,7 +161,7 @@ def create(ctx, name, transport, description, config):
154
161
  )
155
162
  @output_flags()
156
163
  @click.pass_context
157
- def get(ctx, mcp_ref, export):
164
+ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
158
165
  """Get MCP details.
159
166
 
160
167
  Examples:
@@ -172,10 +179,7 @@ def get(ctx, mcp_ref, export):
172
179
  if export:
173
180
  export_path = Path(export)
174
181
  # Auto-detect format from file extension
175
- if export_path.suffix.lower() in [".yaml", ".yml"]:
176
- detected_format = "yaml"
177
- else:
178
- detected_format = "json"
182
+ detected_format = detect_export_format(export_path)
179
183
 
180
184
  # Always export comprehensive data - re-fetch MCP with full details if needed
181
185
  try:
@@ -244,7 +248,7 @@ def get(ctx, mcp_ref, export):
244
248
  @click.argument("mcp_ref")
245
249
  @output_flags()
246
250
  @click.pass_context
247
- def list_tools(ctx, mcp_ref):
251
+ def list_tools(ctx: Any, mcp_ref: str) -> None:
248
252
  """List tools from MCP."""
249
253
  try:
250
254
  client = get_client(ctx)
@@ -263,7 +267,7 @@ def list_tools(ctx, mcp_ref):
263
267
  ]
264
268
 
265
269
  # Transform function for safe dictionary access
266
- def transform_tool(tool):
270
+ def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
267
271
  return {
268
272
  "name": tool.get("name", "N/A"),
269
273
  "description": tool.get("description", "N/A")[:47] + "..."
@@ -289,7 +293,7 @@ def list_tools(ctx, mcp_ref):
289
293
  )
290
294
  @output_flags()
291
295
  @click.pass_context
292
- def connect(ctx, config_file):
296
+ def connect(ctx: Any, config_file: str) -> None:
293
297
  """Connect to MCP using config file."""
294
298
  try:
295
299
  client = get_client(ctx)
@@ -298,7 +302,7 @@ def connect(ctx, config_file):
298
302
  with open(config_file) as f:
299
303
  config = json.load(f)
300
304
 
301
- view = (ctx.obj or {}).get("view", "rich")
305
+ view = get_ctx_value(ctx, "view", "rich")
302
306
  if view != "json":
303
307
  console.print(
304
308
  Text(
@@ -309,7 +313,7 @@ def connect(ctx, config_file):
309
313
  # Test connection using config
310
314
  result = client.mcps.test_mcp_connection_from_config(config)
311
315
 
312
- view = (ctx.obj or {}).get("view", "rich")
316
+ view = get_ctx_value(ctx, "view", "rich")
313
317
  if view == "json":
314
318
  handle_json_output(ctx, result)
315
319
  else:
@@ -332,7 +336,13 @@ def connect(ctx, config_file):
332
336
  @click.option("--config", help="JSON configuration string")
333
337
  @output_flags()
334
338
  @click.pass_context
335
- def update(ctx, mcp_ref, name, description, config):
339
+ def update(
340
+ ctx: Any,
341
+ mcp_ref: str,
342
+ name: str | None,
343
+ description: str | None,
344
+ config: str | None,
345
+ ) -> None:
336
346
  """Update an existing MCP."""
337
347
  try:
338
348
  client = get_client(ctx)
@@ -363,7 +373,7 @@ def update(ctx, mcp_ref, name, description, config):
363
373
 
364
374
  except Exception as e:
365
375
  handle_json_output(ctx, error=e)
366
- if (ctx.obj or {}).get("view") != "json":
376
+ if get_ctx_value(ctx, "view") != "json":
367
377
  display_api_error(e, "MCP update")
368
378
  raise click.ClickException(str(e))
369
379
 
@@ -373,7 +383,7 @@ def update(ctx, mcp_ref, name, description, config):
373
383
  @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
374
384
  @output_flags()
375
385
  @click.pass_context
376
- def delete(ctx, mcp_ref, yes):
386
+ def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
377
387
  """Delete an MCP."""
378
388
  try:
379
389
  client = get_client(ctx)
@@ -398,6 +408,6 @@ def delete(ctx, mcp_ref, yes):
398
408
 
399
409
  except Exception as e:
400
410
  handle_json_output(ctx, error=e)
401
- if (ctx.obj or {}).get("view") != "json":
411
+ if get_ctx_value(ctx, "view") != "json":
402
412
  display_api_error(e, "MCP deletion")
403
413
  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
@@ -33,7 +34,9 @@ from glaip_sdk.cli.io import (
33
34
  from glaip_sdk.cli.resolution import resolve_resource_reference
34
35
  from glaip_sdk.cli.utils import (
35
36
  coerce_to_row,
37
+ detect_export_format,
36
38
  get_client,
39
+ get_ctx_value,
37
40
  output_flags,
38
41
  output_list,
39
42
  output_result,
@@ -45,12 +48,14 @@ console = Console()
45
48
 
46
49
 
47
50
  @click.group(name="tools", no_args_is_help=True)
48
- def tools_group():
51
+ def tools_group() -> None:
49
52
  """Tool management operations."""
50
53
  pass
51
54
 
52
55
 
53
- def _resolve_tool(ctx, client, ref, select=None):
56
+ def _resolve_tool(
57
+ ctx: Any, client: Any, ref: str, select: int | None = None
58
+ ) -> Any | None:
54
59
  """Resolve tool reference (ID or name) with ambiguity handling."""
55
60
  return resolve_resource_reference(
56
61
  ctx,
@@ -90,7 +95,7 @@ def _validate_name_match(provided: str | None, internal: str) -> str:
90
95
  return provided or internal
91
96
 
92
97
 
93
- def _check_duplicate_name(client, tool_name: str) -> None:
98
+ def _check_duplicate_name(client: Any, tool_name: str) -> None:
94
99
  """Raise if a tool with the same name already exists."""
95
100
  try:
96
101
  existing = client.find_tools(name=tool_name)
@@ -111,6 +116,69 @@ def _parse_tags(tags: str | None) -> list[str]:
111
116
  return [t.strip() for t in (tags.split(",") if tags else []) if t.strip()]
112
117
 
113
118
 
119
+ def _handle_import_file(
120
+ import_file: str | None,
121
+ name: str | None,
122
+ description: str | None,
123
+ tags: tuple[str, ...] | None,
124
+ ) -> dict[str, Any]:
125
+ """Handle import file logic and merge with CLI arguments."""
126
+ if import_file:
127
+ import_data = load_resource_from_file(Path(import_file), "tool")
128
+
129
+ # Merge CLI args with imported data
130
+ cli_args = {
131
+ "name": name,
132
+ "description": description,
133
+ "tags": tags,
134
+ }
135
+
136
+ return merge_import_with_cli_args(import_data, cli_args)
137
+ else:
138
+ # No import file - use CLI args directly
139
+ return {
140
+ "name": name,
141
+ "description": description,
142
+ "tags": tags,
143
+ }
144
+
145
+
146
+ def _create_tool_from_file(
147
+ client: Any,
148
+ file_path: str,
149
+ name: str | None,
150
+ description: str | None,
151
+ tags: str | None,
152
+ ) -> Any:
153
+ """Create tool from file upload."""
154
+ with open(file_path, encoding="utf-8") as f:
155
+ code_content = f.read()
156
+
157
+ internal_name = _extract_internal_name(code_content)
158
+ tool_name = _validate_name_match(name, internal_name)
159
+ _check_duplicate_name(client, tool_name)
160
+
161
+ # Upload the plugin code as-is (no rewrite)
162
+ return client.create_tool_from_code(
163
+ name=tool_name,
164
+ code=code_content,
165
+ framework="langchain", # Always langchain
166
+ description=description,
167
+ tags=_parse_tags(tags) if tags else None,
168
+ )
169
+
170
+
171
+ def _validate_creation_parameters(
172
+ file: str | None,
173
+ import_file: str | None,
174
+ ) -> None:
175
+ """Validate required parameters for tool creation."""
176
+ if not file and not import_file:
177
+ raise click.ClickException(
178
+ "A tool file must be provided. Use --file to specify the tool file to upload."
179
+ )
180
+
181
+
114
182
  @tools_group.command(name="list")
115
183
  @output_flags()
116
184
  @click.option(
@@ -121,7 +189,7 @@ def _parse_tags(tags: str | None) -> list[str]:
121
189
  required=False,
122
190
  )
123
191
  @click.pass_context
124
- def list_tools(ctx, tool_type):
192
+ def list_tools(ctx: Any, tool_type: str | None) -> None:
125
193
  """List all tools."""
126
194
  try:
127
195
  client = get_client(ctx)
@@ -135,7 +203,7 @@ def list_tools(ctx, tool_type):
135
203
  ]
136
204
 
137
205
  # Transform function for safe dictionary access
138
- def transform_tool(tool):
206
+ def transform_tool(tool: Any) -> dict[str, Any]:
139
207
  row = coerce_to_row(tool, ["id", "name", "framework"])
140
208
  # Ensure id is always a string
141
209
  row["id"] = str(row["id"])
@@ -152,15 +220,15 @@ def list_tools(ctx, tool_type):
152
220
  @click.option(
153
221
  "--file",
154
222
  type=click.Path(exists=True),
155
- help="Tool file to upload (optional for metadata-only tools)",
223
+ help="Tool file to upload",
156
224
  )
157
225
  @click.option(
158
226
  "--name",
159
- help="Tool name (required for metadata-only tools, extracted from script if file provided)",
227
+ help="Tool name (extracted from script if file provided)",
160
228
  )
161
229
  @click.option(
162
230
  "--description",
163
- help="Tool description (optional - extracted from script if file provided)",
231
+ help="Tool description (extracted from script if file provided)",
164
232
  )
165
233
  @click.option(
166
234
  "--tags",
@@ -174,113 +242,47 @@ def list_tools(ctx, tool_type):
174
242
  )
175
243
  @output_flags()
176
244
  @click.pass_context
177
- def create(ctx, file_arg, file, name, description, tags, import_file):
245
+ def create(
246
+ ctx: Any,
247
+ file_arg: str | None,
248
+ file: str | None,
249
+ name: str | None,
250
+ description: str | None,
251
+ tags: tuple[str, ...] | None,
252
+ import_file: str | None,
253
+ ) -> None:
178
254
  """Create a new tool.
179
255
 
180
256
  Examples:
181
- aip tools create --name "My Tool" --description "A helpful tool"
182
257
  aip tools create tool.py # Create from file
183
258
  aip tools create --import tool.json # Create from exported configuration
184
259
  """
185
260
  try:
186
261
  client = get_client(ctx)
187
262
 
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
- }
263
+ # Allow positional file argument for better DX (matches examples)
264
+ if not file and file_arg:
265
+ file = file_arg
201
266
 
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
- }
267
+ # Handle import file and merge with CLI arguments
268
+ merged_data = _handle_import_file(import_file, name, description, tags)
210
269
 
211
270
  # Extract merged values
212
271
  name = merged_data.get("name")
213
272
  description = merged_data.get("description")
214
273
  tags = merged_data.get("tags")
215
274
 
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
- )
275
+ # Validate required parameters
276
+ _validate_creation_parameters(file, import_file)
227
277
 
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)
278
+ # Create tool from file (either direct file or import file)
279
+ tool = _create_tool_from_file(client, file, name, description, tags)
278
280
 
279
281
  # Handle JSON output
280
282
  handle_json_output(ctx, tool.model_dump())
281
283
 
282
284
  # Handle Rich output
283
- creation_method = "file upload (custom)" if file else "metadata only (native)"
285
+ creation_method = "file upload (custom)"
284
286
  rich_panel = display_creation_success(
285
287
  "Tool",
286
288
  tool.name,
@@ -294,7 +296,7 @@ def create(ctx, file_arg, file, name, description, tags, import_file):
294
296
 
295
297
  except Exception as e:
296
298
  handle_json_output(ctx, error=e)
297
- if ctx.obj.get("view") != "json":
299
+ if get_ctx_value(ctx, "view") != "json":
298
300
  display_api_error(e, "tool creation")
299
301
  raise click.ClickException(str(e))
300
302
 
@@ -309,7 +311,7 @@ def create(ctx, file_arg, file, name, description, tags, import_file):
309
311
  )
310
312
  @output_flags()
311
313
  @click.pass_context
312
- def get(ctx, tool_ref, select, export):
314
+ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None:
313
315
  """Get tool details.
314
316
 
315
317
  Examples:
@@ -327,10 +329,7 @@ def get(ctx, tool_ref, select, export):
327
329
  if export:
328
330
  export_path = Path(export)
329
331
  # Auto-detect format from file extension
330
- if export_path.suffix.lower() in [".yaml", ".yml"]:
331
- detected_format = "yaml"
332
- else:
333
- detected_format = "json"
332
+ detected_format = detect_export_format(export_path)
334
333
 
335
334
  # Always export comprehensive data - re-fetch tool with full details if needed
336
335
  try:
@@ -406,7 +405,13 @@ def get(ctx, tool_ref, select, export):
406
405
  @click.option("--tags", help="Comma-separated tags")
407
406
  @output_flags()
408
407
  @click.pass_context
409
- def update(ctx, tool_id, file, description, tags):
408
+ def update(
409
+ ctx: Any,
410
+ tool_id: str,
411
+ file: str | None,
412
+ description: str | None,
413
+ tags: tuple[str, ...] | None,
414
+ ) -> None:
410
415
  """Update a tool (code or metadata)."""
411
416
  try:
412
417
  client = get_client(ctx)
@@ -453,7 +458,7 @@ def update(ctx, tool_id, file, description, tags):
453
458
 
454
459
  except Exception as e:
455
460
  handle_json_output(ctx, error=e)
456
- if ctx.obj.get("view") != "json":
461
+ if get_ctx_value(ctx, "view") != "json":
457
462
  display_api_error(e, "tool update")
458
463
  raise click.ClickException(str(e))
459
464
 
@@ -463,7 +468,7 @@ def update(ctx, tool_id, file, description, tags):
463
468
  @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
464
469
  @output_flags()
465
470
  @click.pass_context
466
- def delete(ctx, tool_id, yes):
471
+ def delete(ctx: Any, tool_id: str, yes: bool) -> None:
467
472
  """Delete a tool."""
468
473
  try:
469
474
  client = get_client(ctx)
@@ -491,7 +496,7 @@ def delete(ctx, tool_id, yes):
491
496
 
492
497
  except Exception as e:
493
498
  handle_json_output(ctx, error=e)
494
- if ctx.obj.get("view") != "json":
499
+ if get_ctx_value(ctx, "view") != "json":
495
500
  display_api_error(e, "tool deletion")
496
501
  raise click.ClickException(str(e))
497
502
 
@@ -500,13 +505,13 @@ def delete(ctx, tool_id, yes):
500
505
  @click.argument("tool_id")
501
506
  @output_flags()
502
507
  @click.pass_context
503
- def script(ctx, tool_id):
508
+ def script(ctx: Any, tool_id: str) -> None:
504
509
  """Get tool script content."""
505
510
  try:
506
511
  client = get_client(ctx)
507
512
  script_content = client.get_tool_script(tool_id)
508
513
 
509
- if ctx.obj.get("view") == "json":
514
+ if get_ctx_value(ctx, "view") == "json":
510
515
  click.echo(json.dumps({"script": script_content}, indent=2))
511
516
  else:
512
517
  console.print(f"[green]📜 Tool Script for '{tool_id}':[/green]")
@@ -514,6 +519,6 @@ def script(ctx, tool_id):
514
519
 
515
520
  except Exception as e:
516
521
  handle_json_output(ctx, error=e)
517
- if ctx.obj.get("view") != "json":
522
+ if get_ctx_value(ctx, "view") != "json":
518
523
  console.print(Text(f"[red]Error getting tool script: {e}[/red]"))
519
524
  raise click.ClickException(str(e))