glaip-sdk 0.0.9__py3-none-any.whl → 0.0.11__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/branding.py CHANGED
@@ -4,7 +4,7 @@ Simple, friendly CLI branding for the GL AIP (GDP Labs AI Agent Package) SDK.
4
4
 
5
5
  - Package name: GL AIP (GDP Labs AI Agent Package)
6
6
  - Version: auto-detected (AIP_VERSION env or importlib.metadata), or passed in
7
- - Colors: blue / bright_blue, with NO_COLOR/AIP_NO_COLOR fallbacks
7
+ - Colors: GDP Labs brand palette with NO_COLOR/AIP_NO_COLOR fallbacks
8
8
 
9
9
  Author:
10
10
  Raymond Christopher (raymond.christopher@gdplabs.id)
@@ -30,9 +30,13 @@ except Exception: # pragma: no cover
30
30
  PackageNotFoundError = Exception
31
31
 
32
32
 
33
- # ---- minimal, readable styles (light blue + white theme) -----------------
34
- PRIMARY = "#15a2d8" # GDP Labs brand blue
35
- BORDER = PRIMARY # Keep borders aligned with brand tone
33
+ # ---- GDP Labs Brand Color Palette -----------------------------------------
34
+ PRIMARY = "#004987" # Primary brand blue (dark blue)
35
+ SECONDARY_DARK = "#003A5C" # Darkest variant for emphasis
36
+ SECONDARY_MEDIUM = "#005CB8" # Medium variant for UI elements
37
+ SECONDARY_LIGHT = "#40B4E5" # Light variant for highlights
38
+
39
+ BORDER = PRIMARY # Keep borders aligned with primary brand tone
36
40
  TITLE_STYLE = f"bold {PRIMARY}"
37
41
  LABEL = "bold"
38
42
 
glaip_sdk/cli/auth.py ADDED
@@ -0,0 +1,414 @@
1
+ """Authentication export helpers for MCP CLI commands.
2
+
3
+ This module provides utilities for preparing authentication data for export,
4
+ including interactive secret capture and placeholder generation.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ from collections.abc import Iterable, Mapping
11
+ from typing import Any
12
+
13
+ import click
14
+ from rich.console import Console
15
+
16
+
17
+ def prepare_authentication_export(
18
+ auth: dict[str, Any] | None,
19
+ *,
20
+ prompt_for_secrets: bool,
21
+ placeholder: str,
22
+ console: Console,
23
+ ) -> dict[str, Any] | None:
24
+ """Prepare authentication data for export with secret handling.
25
+
26
+ This function processes authentication objects from MCP resources and prepares
27
+ them for export. It handles secret capture (interactive or placeholder mode),
28
+ reconstructs proper authentication structures from helper metadata, and ensures
29
+ helper metadata doesn't leak into the final export.
30
+
31
+ Args:
32
+ auth: Authentication dictionary from an MCP resource. May contain helper
33
+ metadata like ``header_keys`` that should be consumed and removed.
34
+ prompt_for_secrets: If True, interactively prompt for missing secrets.
35
+ If False, use ``placeholder`` automatically.
36
+ placeholder: Placeholder text to use for missing secrets when not prompting.
37
+ console: Rich ``Console`` instance for user interaction and warnings.
38
+
39
+ Returns:
40
+ A prepared authentication dictionary ready for export, or ``None`` if
41
+ ``auth`` is ``None``.
42
+
43
+ Notes:
44
+ - Helper metadata (for example, ``header_keys``) is consumed to rebuild
45
+ structures but never appears in the final output.
46
+ - When ``prompt_for_secrets`` is False and stdin is not a TTY, a warning is
47
+ logged.
48
+ - Empty user input during prompts defaults to the placeholder value.
49
+ """
50
+ if auth is None:
51
+ return None
52
+
53
+ auth_type = auth.get("type")
54
+
55
+ # Handle no-auth case
56
+ if auth_type == "no-auth":
57
+ return {"type": "no-auth"}
58
+
59
+ # Handle bearer-token authentication
60
+ if auth_type == "bearer-token":
61
+ return _prepare_bearer_token_auth(
62
+ auth, prompt_for_secrets, placeholder, console
63
+ )
64
+
65
+ # Handle api-key authentication
66
+ if auth_type == "api-key":
67
+ return _prepare_api_key_auth(auth, prompt_for_secrets, placeholder, console)
68
+
69
+ # Handle custom-header authentication
70
+ if auth_type == "custom-header":
71
+ return _prepare_custom_header_auth(
72
+ auth, prompt_for_secrets, placeholder, console
73
+ )
74
+
75
+ # Unknown auth type - return as-is but strip helper metadata
76
+ result = auth.copy()
77
+ result.pop("header_keys", None)
78
+ return result
79
+
80
+
81
+ def _get_token_value(
82
+ prompt_for_secrets: bool, placeholder: str, console: Console
83
+ ) -> str:
84
+ """Get bearer token value either by prompting or using a placeholder.
85
+
86
+ Args:
87
+ prompt_for_secrets: If True, prompt for the token value.
88
+ placeholder: Placeholder to use when not prompting or when input is empty.
89
+ console: Rich ``Console`` used to display informational messages.
90
+
91
+ Returns:
92
+ The token string, either provided by the user or the placeholder.
93
+ """
94
+ if prompt_for_secrets:
95
+ console.print(
96
+ "[yellow]Bearer token is missing or redacted. "
97
+ "Please provide the token.[/yellow]"
98
+ )
99
+ token_value = click.prompt(
100
+ "Bearer token (leave blank for placeholder)",
101
+ default="",
102
+ show_default=False,
103
+ )
104
+ return token_value.strip() or placeholder
105
+
106
+ if not click.get_text_stream("stdin").isatty():
107
+ console.print(
108
+ "[yellow]⚠️ Non-interactive mode: "
109
+ "using placeholder for bearer token[/yellow]"
110
+ )
111
+ return placeholder
112
+
113
+
114
+ def _build_bearer_headers(auth: dict[str, Any], token_value: str) -> dict[str, str]:
115
+ """Build headers for bearer token authentication.
116
+
117
+ Args:
118
+ auth: Original authentication dictionary which may include ``header_keys``.
119
+ token_value: The token value to embed into the headers.
120
+
121
+ Returns:
122
+ A dictionary of HTTP headers including the Authorization header when
123
+ applicable.
124
+ """
125
+ header_keys = auth.get("header_keys", ["Authorization"])
126
+ headers = {}
127
+ for key in header_keys:
128
+ # Prepend "Bearer " if this is Authorization header
129
+ if key.lower() == "authorization":
130
+ headers[key] = f"Bearer {token_value}"
131
+ else:
132
+ headers[key] = token_value
133
+ return headers
134
+
135
+
136
+ def _prepare_bearer_token_auth(
137
+ auth: dict[str, Any],
138
+ prompt_for_secrets: bool,
139
+ placeholder: str,
140
+ console: Console,
141
+ ) -> dict[str, Any]:
142
+ """Prepare bearer-token authentication for export.
143
+
144
+ Args:
145
+ auth: Original authentication dictionary.
146
+ prompt_for_secrets: Whether to prompt for secrets.
147
+ placeholder: Placeholder value for secrets.
148
+ console: Rich ``Console`` for interaction.
149
+
150
+ Returns:
151
+ A prepared ``bearer-token`` authentication dictionary.
152
+ """
153
+ # Check if token exists and is not masked
154
+ token = auth.get("token")
155
+ has_valid_token = token and token not in (None, "", "***", "REDACTED")
156
+
157
+ # If we have a valid token, use it
158
+ if has_valid_token:
159
+ return {"type": "bearer-token", "token": token}
160
+
161
+ # Get token value (prompt or placeholder)
162
+ token_value = _get_token_value(prompt_for_secrets, placeholder, console)
163
+
164
+ # Check if original had headers structure
165
+ if "headers" in auth or "header_keys" in auth:
166
+ headers = _build_bearer_headers(auth, token_value)
167
+ return {"type": "bearer-token", "headers": headers}
168
+
169
+ # Use token field structure
170
+ return {"type": "bearer-token", "token": token_value}
171
+
172
+
173
+ def _extract_api_key_name(auth: dict[str, Any]) -> str | None:
174
+ """Extract the API key name from an authentication dictionary.
175
+
176
+ Args:
177
+ auth: Authentication dictionary that may contain ``key`` or ``header_keys``.
178
+
179
+ Returns:
180
+ The API key name if available, otherwise ``None``.
181
+ """
182
+ key_name = auth.get("key")
183
+ if not key_name and "header_keys" in auth:
184
+ header_keys = auth["header_keys"]
185
+ if isinstance(header_keys, list) and header_keys:
186
+ key_name = header_keys[0]
187
+ return key_name
188
+
189
+
190
+ def _get_api_key_value(
191
+ key_name: str | None,
192
+ prompt_for_secrets: bool,
193
+ placeholder: str,
194
+ console: Console,
195
+ ) -> str:
196
+ """Get API key value either by prompting or using a placeholder.
197
+
198
+ Args:
199
+ key_name: The name of the API key; used in prompt messages.
200
+ prompt_for_secrets: If True, prompt for the API key value.
201
+ placeholder: Placeholder to use when not prompting or when input is empty.
202
+ console: Rich ``Console`` used to display informational messages.
203
+
204
+ Returns:
205
+ The API key value, either provided by the user or the placeholder.
206
+ """
207
+ if prompt_for_secrets:
208
+ console.print(
209
+ f"[yellow]API key value for '{key_name}' is missing or redacted.[/yellow]"
210
+ )
211
+ key_value = click.prompt(
212
+ f"API key value for '{key_name}' (leave blank for placeholder)",
213
+ default="",
214
+ show_default=False,
215
+ )
216
+ return key_value.strip() or placeholder
217
+
218
+ if not click.get_text_stream("stdin").isatty():
219
+ console.print(
220
+ f"[yellow]⚠️ Non-interactive mode: "
221
+ f"using placeholder for API key '{key_name}'[/yellow]"
222
+ )
223
+ return placeholder
224
+
225
+
226
+ def _build_api_key_headers(
227
+ auth: dict[str, Any], key_name: str | None, key_value: str
228
+ ) -> dict[str, str]:
229
+ """Build headers for API key authentication.
230
+
231
+ Args:
232
+ auth: Original authentication dictionary which may include ``header_keys``.
233
+ key_name: The header key name if present.
234
+ key_value: The API key value to populate.
235
+
236
+ Returns:
237
+ A dictionary of HTTP headers for API key authentication.
238
+ """
239
+ header_keys = auth.get("header_keys", [key_name] if key_name else [])
240
+ return {key: key_value for key in header_keys}
241
+
242
+
243
+ def _prepare_api_key_auth(
244
+ auth: dict[str, Any],
245
+ prompt_for_secrets: bool,
246
+ placeholder: str,
247
+ console: Console,
248
+ ) -> dict[str, Any]:
249
+ """Prepare api-key authentication for export.
250
+
251
+ Args:
252
+ auth: Original authentication dictionary.
253
+ prompt_for_secrets: Whether to prompt for secrets.
254
+ placeholder: Placeholder value for secrets.
255
+ console: Rich ``Console`` for interaction.
256
+
257
+ Returns:
258
+ A prepared ``api-key`` authentication dictionary.
259
+ """
260
+ # Extract key name and value
261
+ key_name = _extract_api_key_name(auth)
262
+ key_value = auth.get("value")
263
+
264
+ # Check if we have a valid value
265
+ has_valid_value = key_value and key_value not in (None, "", "***", "REDACTED")
266
+
267
+ # Capture or use placeholder for value
268
+ if not has_valid_value:
269
+ key_value = _get_api_key_value(
270
+ key_name, prompt_for_secrets, placeholder, console
271
+ )
272
+
273
+ # Check if original had headers structure
274
+ if "headers" in auth or "header_keys" in auth:
275
+ headers = _build_api_key_headers(auth, key_name, key_value)
276
+ return {"type": "api-key", "headers": headers}
277
+
278
+ # Use key/value field structure
279
+ return {"type": "api-key", "key": key_name, "value": key_value}
280
+
281
+
282
+ def _prepare_custom_header_auth(
283
+ auth: dict[str, Any],
284
+ prompt_for_secrets: bool,
285
+ placeholder: str,
286
+ console: Console,
287
+ ) -> dict[str, Any]:
288
+ """Prepare custom-header authentication for export.
289
+
290
+ Args:
291
+ auth: Original authentication dictionary.
292
+ prompt_for_secrets: Whether to prompt for header values.
293
+ placeholder: Placeholder value when not prompting or input is empty.
294
+ console: Rich ``Console`` for interaction.
295
+
296
+ Returns:
297
+ A prepared ``custom-header`` authentication dictionary.
298
+ """
299
+ existing_headers: dict[str, Any] = auth.get("headers", {})
300
+ header_names = _extract_header_names(existing_headers, auth.get("header_keys", []))
301
+
302
+ if not header_names:
303
+ return {"type": "custom-header", "headers": {}}
304
+
305
+ headers = _build_custom_headers(
306
+ existing_headers=existing_headers,
307
+ header_names=header_names,
308
+ prompt_for_secrets=prompt_for_secrets,
309
+ placeholder=placeholder,
310
+ console=console,
311
+ )
312
+
313
+ return {"type": "custom-header", "headers": headers}
314
+
315
+
316
+ def _extract_header_names(
317
+ existing_headers: Mapping[str, Any] | None, header_keys: Iterable[str] | None
318
+ ) -> list[str]:
319
+ """Extract the list of header names to process.
320
+
321
+ Args:
322
+ existing_headers: Existing headers mapping from the auth object.
323
+ header_keys: Optional helper metadata listing header names.
324
+
325
+ Returns:
326
+ A list of header names to process.
327
+ """
328
+ if existing_headers:
329
+ return list(existing_headers.keys())
330
+ if header_keys:
331
+ return list(header_keys)
332
+ return []
333
+
334
+
335
+ def _is_valid_secret(value: Any) -> bool:
336
+ """Determine whether a secret value is present and not masked.
337
+
338
+ Args:
339
+ value: The value to test.
340
+
341
+ Returns:
342
+ True if the value is non-empty and not one of the masked placeholders.
343
+ """
344
+ return bool(value) and value not in (None, "", "***", "REDACTED")
345
+
346
+
347
+ def _prompt_or_placeholder(
348
+ name: str,
349
+ prompt_for_secrets: bool,
350
+ placeholder: str,
351
+ console: Console,
352
+ ) -> str:
353
+ """Prompt for a header value or return the placeholder when not prompting.
354
+
355
+ Args:
356
+ name: Header name used in prompt messages.
357
+ prompt_for_secrets: If True, prompt for the value interactively.
358
+ placeholder: Placeholder value used when not prompting or empty input.
359
+ console: Rich ``Console`` instance for user-facing messages.
360
+
361
+ Returns:
362
+ The provided value or the placeholder.
363
+ """
364
+ if prompt_for_secrets:
365
+ console.print(f"[yellow]Header '{name}' is missing or redacted.[/yellow]")
366
+ value = click.prompt(
367
+ f"Value for header '{name}' (leave blank for placeholder)",
368
+ default="",
369
+ show_default=False,
370
+ )
371
+ return value.strip() or placeholder
372
+
373
+ if not click.get_text_stream("stdin").isatty():
374
+ console.print(
375
+ f"[yellow]⚠️ Non-interactive mode: using placeholder for header '{name}'[/yellow]"
376
+ )
377
+ return placeholder
378
+
379
+
380
+ def _build_custom_headers(
381
+ *,
382
+ existing_headers: Mapping[str, Any],
383
+ header_names: Iterable[str],
384
+ prompt_for_secrets: bool,
385
+ placeholder: str,
386
+ console: Console,
387
+ ) -> dict[str, str]:
388
+ """Build a headers mapping for custom-header authentication.
389
+
390
+ Args:
391
+ existing_headers: Existing headers mapping from the auth object.
392
+ header_names: Header names to process.
393
+ prompt_for_secrets: Whether to prompt for missing values.
394
+ placeholder: Placeholder to use for missing or masked values.
395
+ console: Rich ``Console`` used for prompt/warning messages.
396
+
397
+ Returns:
398
+ A dictionary mapping header names to resolved values.
399
+ """
400
+ headers: dict[str, str] = {}
401
+ for name in header_names:
402
+ existing_value = existing_headers.get(name)
403
+ if _is_valid_secret(existing_value):
404
+ headers[name] = str(existing_value)
405
+ continue
406
+
407
+ headers[name] = _prompt_or_placeholder(
408
+ name=name,
409
+ prompt_for_secrets=prompt_for_secrets,
410
+ placeholder=placeholder,
411
+ console=console,
412
+ )
413
+
414
+ return headers
@@ -416,8 +416,19 @@ def list_agents(
416
416
  return row
417
417
 
418
418
  # Use fuzzy picker for interactive agent selection and details (default behavior)
419
- # Skip if --simple flag is used
420
- if not simple and console.is_terminal and os.isatty(1) and len(agents) > 0:
419
+ # Skip if --simple flag is used, a name filter is applied, or non-rich output is requested
420
+ ctx_obj = ctx.obj if isinstance(getattr(ctx, "obj", None), dict) else {}
421
+ current_view = ctx_obj.get("view")
422
+ interactive_enabled = (
423
+ not simple
424
+ and name is None
425
+ and current_view not in {"json", "plain", "md"}
426
+ and console.is_terminal
427
+ and os.isatty(1)
428
+ and len(agents) > 0
429
+ )
430
+
431
+ if interactive_enabled:
421
432
  picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
422
433
  if picked_agent:
423
434
  _display_agent_details(ctx, client, picked_agent)
@@ -426,7 +437,18 @@ def list_agents(
426
437
  return
427
438
 
428
439
  # Show simple table (either --simple flag or non-interactive)
429
- output_list(ctx, agents, "🤖 Available Agents", columns, transform_agent)
440
+ output_list(
441
+ ctx,
442
+ agents,
443
+ "🤖 Available Agents",
444
+ columns,
445
+ transform_agent,
446
+ skip_picker=simple
447
+ or any(
448
+ param is not None for param in (agent_type, framework, name, version)
449
+ ),
450
+ use_pager=False,
451
+ )
430
452
 
431
453
  except Exception as e:
432
454
  raise click.ClickException(str(e))