glaip-sdk 0.5.1__py3-none-any.whl → 0.5.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.
@@ -106,6 +106,9 @@ def list_accounts(output_json: bool) -> None:
106
106
  if active_account:
107
107
  console.print(f"\n[{SUCCESS_STYLE}]Active account[/]: {active_account}")
108
108
 
109
+ # Show hint for updating accounts
110
+ console.print(f"\n[{INFO}]💡 Tip[/]: To update an account's URL or key, use: [bold]aip accounts edit <name>[/bold]")
111
+
109
112
 
110
113
  def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) -> dict[str, str] | None:
111
114
  """Check if account exists and handle overwrite logic.
@@ -128,13 +131,19 @@ def _check_account_overwrite(name: str, store: AccountStore, overwrite: bool) ->
128
131
  return existing
129
132
 
130
133
 
131
- def _get_credentials_non_interactive(url: str, read_key_from_stdin: bool, name: str) -> tuple[str, str]:
134
+ def _get_credentials_non_interactive(
135
+ url: str,
136
+ read_key_from_stdin: bool,
137
+ name: str,
138
+ command_name: str = "aip accounts add",
139
+ ) -> tuple[str, str]:
132
140
  """Get credentials in non-interactive mode.
133
141
 
134
142
  Args:
135
143
  url: API URL from flag.
136
144
  read_key_from_stdin: Whether to read key from stdin.
137
145
  name: Account name (for error messages).
146
+ command_name: Command name for guidance text.
138
147
 
139
148
  Returns:
140
149
  Tuple of (api_url, api_key).
@@ -147,7 +156,7 @@ def _get_credentials_non_interactive(url: str, read_key_from_stdin: bool, name:
147
156
  return url, sys.stdin.read().strip()
148
157
  console.print(
149
158
  f"[{ERROR_STYLE}]Error: --key requires stdin input. "
150
- f"Use: cat key.txt | aip accounts add {name} --url {url} --key[/]",
159
+ f"Use: cat key.txt | {command_name} {name} --url {url} --key[/]",
151
160
  )
152
161
  raise click.Abort()
153
162
  # URL provided, prompt for key
@@ -178,6 +187,88 @@ def _get_credentials_interactive(read_key_from_stdin: bool, existing: dict[str,
178
187
  return _prompt_account_inputs(existing)
179
188
 
180
189
 
190
+ def _handle_key_rotation(
191
+ name: str,
192
+ existing_url: str,
193
+ command_name: str,
194
+ ) -> tuple[str, str]:
195
+ """Handle key rotation using stored URL.
196
+
197
+ Args:
198
+ name: Account name (for error messages).
199
+ existing_url: Existing account URL.
200
+ command_name: Command name for error messages.
201
+
202
+ Returns:
203
+ Tuple of (api_url, api_key).
204
+
205
+ Raises:
206
+ click.Abort: If existing URL is missing.
207
+ """
208
+ if not existing_url:
209
+ console.print(f"[{ERROR_STYLE}]Error: Account '{name}' is missing an API URL. Provide --url to set it.[/]")
210
+ raise click.Abort()
211
+ return _get_credentials_non_interactive(existing_url, True, name, command_name)
212
+
213
+
214
+ def _preserve_existing_values(
215
+ api_url: str,
216
+ api_key: str,
217
+ existing_url: str,
218
+ existing_key: str,
219
+ ) -> tuple[str, str]:
220
+ """Preserve stored values when blank input is provided during edit.
221
+
222
+ Args:
223
+ api_url: Collected API URL.
224
+ api_key: Collected API key.
225
+ existing_url: Existing account URL.
226
+ existing_key: Existing account key.
227
+
228
+ Returns:
229
+ Tuple of (api_url, api_key) with preserved values.
230
+ """
231
+ if not api_url and existing_url:
232
+ api_url = existing_url
233
+ if not api_key and existing_key:
234
+ api_key = existing_key
235
+ return api_url, api_key
236
+
237
+
238
+ def _collect_credentials_from_inputs(
239
+ url: str | None,
240
+ read_key_from_stdin: bool,
241
+ name: str,
242
+ existing: dict[str, str] | None,
243
+ command_name: str,
244
+ existing_url: str,
245
+ ) -> tuple[str, str]:
246
+ """Collect credentials based on input flags and existing data.
247
+
248
+ Args:
249
+ url: Optional URL from flag.
250
+ read_key_from_stdin: Whether to read key from stdin.
251
+ name: Account name (for error messages).
252
+ existing: Existing account data.
253
+ command_name: Command name for error messages.
254
+ existing_url: Existing account URL.
255
+
256
+ Returns:
257
+ Tuple of (api_url, api_key).
258
+ """
259
+ if url and read_key_from_stdin:
260
+ # Non-interactive: URL from flag, key from stdin
261
+ return _get_credentials_non_interactive(url, True, name, command_name)
262
+ if url:
263
+ # URL provided, prompt for key
264
+ return _get_credentials_non_interactive(url, False, name, command_name)
265
+ if read_key_from_stdin and existing:
266
+ # Key rotation using stored URL
267
+ return _handle_key_rotation(name, existing_url, command_name)
268
+ # Fully interactive or error case
269
+ return _get_credentials_interactive(read_key_from_stdin, existing)
270
+
271
+
181
272
  def _collect_account_credentials(
182
273
  url: str | None,
183
274
  read_key_from_stdin: bool,
@@ -198,15 +289,16 @@ def _collect_account_credentials(
198
289
  Raises:
199
290
  click.Abort: If credentials cannot be collected or are invalid.
200
291
  """
201
- if url and read_key_from_stdin:
202
- # Non-interactive: URL from flag, key from stdin
203
- api_url, api_key = _get_credentials_non_interactive(url, True, name)
204
- elif url:
205
- # URL provided, prompt for key
206
- api_url, api_key = _get_credentials_non_interactive(url, False, name)
207
- else:
208
- # Fully interactive or error case
209
- api_url, api_key = _get_credentials_interactive(read_key_from_stdin, existing)
292
+ command_name = "aip accounts edit" if existing else "aip accounts add"
293
+ existing_url = existing.get("api_url", "") if existing else ""
294
+ existing_key = existing.get("api_key", "") if existing else ""
295
+
296
+ api_url, api_key = _collect_credentials_from_inputs(
297
+ url, read_key_from_stdin, name, existing, command_name, existing_url
298
+ )
299
+
300
+ # Preserve stored values when blank input is provided during edit
301
+ api_url, api_key = _preserve_existing_values(api_url, api_key, existing_url, existing_key)
210
302
 
211
303
  if not api_url or not api_key:
212
304
  console.print(f"[{ERROR_STYLE}]Error: Both API URL and API key are required.[/]")
@@ -235,12 +327,15 @@ def add_account(
235
327
  read_key_from_stdin: bool,
236
328
  overwrite: bool,
237
329
  ) -> None:
238
- """Add or update an account profile.
330
+ """Add a new account profile.
239
331
 
240
332
  NAME is the account name (1-32 chars, alphanumeric, dash, underscore).
241
333
 
242
334
  By default, this command runs interactively, prompting for API URL and key.
243
335
  For non-interactive use, both --url and --key (stdin) are required.
336
+
337
+ If the account already exists, use --yes to overwrite without prompting.
338
+ To update an existing account, use [bold]aip accounts edit <name>[/bold] instead.
244
339
  """
245
340
  store = get_account_store()
246
341
 
@@ -263,6 +358,55 @@ def add_account(
263
358
  raise click.Abort() from e
264
359
 
265
360
 
361
+ @accounts_group.command("edit")
362
+ @click.argument("name")
363
+ @click.option("--url", help="API URL (optional, leave blank to keep current)")
364
+ @click.option(
365
+ "--key",
366
+ "read_key_from_stdin",
367
+ is_flag=True,
368
+ help="Read API key from stdin (secure, for scripts). Uses stored URL unless --url is provided.",
369
+ )
370
+ def edit_account(
371
+ name: str,
372
+ url: str | None,
373
+ read_key_from_stdin: bool,
374
+ ) -> None:
375
+ """Edit an existing account profile's URL or key.
376
+
377
+ NAME is the account name to edit.
378
+
379
+ By default, this command runs interactively, showing current values and
380
+ prompting for new ones. Leave fields blank to keep current values.
381
+
382
+ For non-interactive use, provide --url to change the URL, --key (stdin) to rotate the key,
383
+ or both. Stored values are reused for any fields not provided.
384
+ """
385
+ store = get_account_store()
386
+
387
+ # Account must exist for edit
388
+ existing = store.get_account(name)
389
+ if not existing:
390
+ console.print(f"[{ERROR_STYLE}]Error: Account '{name}' not found.[/]")
391
+ console.print(f"Use [bold]aip accounts add {name}[/bold] to create a new account.")
392
+ raise click.Abort()
393
+
394
+ # Collect credentials (will pre-fill existing values in interactive mode)
395
+ api_url, api_key = _collect_account_credentials(url, read_key_from_stdin, name, existing)
396
+
397
+ # Save account
398
+ try:
399
+ store.add_account(name, api_url, api_key, overwrite=True)
400
+ console.print(Text(f"✅ Account '{name}' updated successfully", style=SUCCESS_STYLE))
401
+ _print_active_account_footer(store)
402
+ except InvalidAccountNameError as e:
403
+ console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
404
+ raise click.Abort() from e
405
+ except AccountStoreError as e:
406
+ console.print(f"[{ERROR_STYLE}]Error: {e}[/]")
407
+ raise click.Abort() from e
408
+
409
+
266
410
  @accounts_group.command("use")
267
411
  @click.argument("name")
268
412
  def use_account(name: str) -> None:
@@ -281,7 +425,8 @@ def use_account(name: str) -> None:
281
425
 
282
426
  if not url or not api_key:
283
427
  console.print(
284
- f"[{ERROR_STYLE}]Error: Account '{name}' is missing credentials. Re-run 'aip accounts add {name}'.[/]"
428
+ f"[{ERROR_STYLE}]Error: Account '{name}' is missing credentials. "
429
+ f"Use [bold]aip accounts edit {name}[/bold] to update credentials.[/]"
285
430
  )
286
431
  raise click.Abort()
287
432
 
@@ -594,9 +594,12 @@ def list_agents(
594
594
  f"{ICON_AGENT} Available Agents",
595
595
  columns,
596
596
  transform_agent,
597
- skip_picker=picker_attempted
598
- or simple
599
- or any(param is not None for param in (agent_type, framework, name, version)),
597
+ skip_picker=(
598
+ not interactive_enabled
599
+ or picker_attempted
600
+ or simple
601
+ or any(param is not None for param in (agent_type, framework, name, version))
602
+ ),
600
603
  use_pager=False,
601
604
  )
602
605
 
@@ -1387,6 +1390,12 @@ def update(
1387
1390
 
1388
1391
  if merged_data:
1389
1392
  _handle_update_import_config(import_file, merged_data, update_data)
1393
+ # Ensure instruction from import file is included if not already set via CLI
1394
+ # This handles the case where instruction is None in CLI args but exists in import file
1395
+ if import_file and (instruction is None or "instruction" not in update_data):
1396
+ import_instruction = merged_data.get("instruction")
1397
+ if import_instruction is not None:
1398
+ update_data["instruction"] = import_instruction
1390
1399
 
1391
1400
  if not update_data:
1392
1401
  raise click.ClickException("No update fields specified")
@@ -5,12 +5,25 @@ Authors:
5
5
  """
6
6
 
7
7
  import getpass
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ import threading
13
+ from pathlib import Path
14
+ from typing import Any
8
15
 
9
16
  import click
10
17
  from rich.console import Console
11
18
  from rich.text import Text
12
19
 
13
- from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
20
+ from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO, NEUTRAL, SUCCESS_STYLE, WARNING_STYLE
21
+
22
+ # Optional import for gitignore support; warn when missing to avoid silent expansion
23
+ try:
24
+ import pathspec # type: ignore[import-untyped] # noqa: PLC0415
25
+ except ImportError:
26
+ pathspec = None # type: ignore[assignment]
14
27
  from glaip_sdk.cli.account_store import get_account_store
15
28
  from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
16
29
  from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
@@ -18,27 +31,114 @@ from glaip_sdk.cli.hints import format_command_hint
18
31
  from glaip_sdk.cli.masking import mask_api_key_display
19
32
  from glaip_sdk.cli.rich_helpers import markup_text
20
33
  from glaip_sdk.cli.utils import command_hint
21
- from glaip_sdk.icons import ICON_TOOL
22
34
  from glaip_sdk.rich_components import AIPTable
23
35
 
24
36
  console = Console()
37
+ stderr_console = Console(file=sys.stderr)
38
+ _PATHSPEC_WARNED = False
39
+ _PATHSPEC_WARNED_LOCK = threading.Lock()
40
+
41
+ # Hard deprecation banner for legacy config commands (v0.6.x)
42
+ CONFIG_HARD_DEPRECATION_MSG = (
43
+ f"[{WARNING_STYLE}]⚠️ DEPRECATED: 'aip config ...' commands will be removed in v0.7.0. "
44
+ "Use 'aip accounts ...' (list/add/use/remove/edit) or 'aip configure' for the wizard. "
45
+ "Set AIP_ENABLE_LEGACY_CONFIG=1 to temporarily re-enable these commands.[/]"
46
+ )
25
47
 
26
- # Shared deprecation banner for legacy config commands
27
- CONFIG_DEPRECATION_MSG = (
28
- f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in a future release. "
29
- "Use 'aip accounts ...' (list/add/use/remove) or 'aip configure' for the wizard.[/]"
48
+ # Soft deprecation banner (for when env flag is set)
49
+ CONFIG_SOFT_DEPRECATION_MSG = (
50
+ f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in v0.7.0. "
51
+ "Use 'aip accounts ...' (list/add/use/remove/edit) or 'aip configure' for the wizard.[/]"
30
52
  )
31
53
 
54
+ # Target removal version
55
+ TARGET_REMOVAL_VERSION = "v0.7.0"
56
+
57
+ # Command hint constant
58
+ CONFIG_CONFIGURE_HINT = "config configure"
59
+ _DEFAULT_EXCLUDE_DIRS = {
60
+ ".git",
61
+ "node_modules",
62
+ ".venv",
63
+ "venv",
64
+ ".tox",
65
+ "build",
66
+ "dist",
67
+ "__pycache__",
68
+ ".mypy_cache",
69
+ ".pytest_cache",
70
+ }
71
+ _MAX_SCAN_FILE_SIZE = 2 * 1024 * 1024 # 2MB cap for default scans
72
+
73
+
74
+ def _is_legacy_config_enabled() -> bool:
75
+ """Check if legacy config commands are enabled via environment variable."""
76
+ env_value = os.environ.get("AIP_ENABLE_LEGACY_CONFIG", "").strip().lower()
77
+ return env_value in ("1", "true", "yes", "on")
78
+
32
79
 
33
80
  def _print_config_deprecation() -> None:
34
81
  """Print a standardized deprecation warning for legacy config commands."""
35
- console.print(CONFIG_DEPRECATION_MSG)
82
+ if _is_legacy_config_enabled():
83
+ # Soft deprecation when env flag is set
84
+ stderr_console.print(CONFIG_SOFT_DEPRECATION_MSG)
85
+ else:
86
+ # Hard deprecation when env flag is not set
87
+ stderr_console.print(CONFIG_HARD_DEPRECATION_MSG)
88
+
89
+
90
+ def _check_legacy_config_gate() -> bool:
91
+ """Return True if legacy config commands are allowed; print banner otherwise."""
92
+ if not _is_legacy_config_enabled():
93
+ stderr_console.print(CONFIG_HARD_DEPRECATION_MSG)
94
+ return False
95
+ return True
96
+
97
+
98
+ def _enforce_legacy_config_gate() -> None:
99
+ """CLI-only gate: exit with code 0 when legacy commands are disabled."""
100
+ if not _check_legacy_config_gate():
101
+ # Spec requires non-breaking exit after banner
102
+ sys.exit(0)
103
+
104
+
105
+ def _emit_telemetry_event(_event_name: str, properties: dict[str, Any] | None = None) -> None:
106
+ """Emit telemetry event for legacy command usage tracking.
107
+
108
+ This is a stub implementation that can be connected to a real telemetry system.
109
+ For now, it's a no-op but structured to allow easy integration.
110
+
111
+ Args:
112
+ _event_name: Name of the telemetry event (prefixed with _ to indicate unused for now).
113
+ properties: Optional event properties dictionary.
114
+
115
+ Note:
116
+ TODO: Connect to actual telemetry system when available.
117
+ """
118
+ if properties is None:
119
+ properties = {}
120
+ # Mark as intentionally unused until telemetry system is integrated
121
+ del _event_name, properties
36
122
 
37
123
 
38
124
  @click.group()
39
125
  def config_group() -> None:
40
- """Configuration management operations."""
126
+ """Configuration management operations (deprecated).
127
+
128
+ These commands are deprecated and will be removed in v0.7.0.
129
+ Use 'aip accounts ...' commands instead.
130
+ Set AIP_ENABLE_LEGACY_CONFIG=1 to temporarily re-enable.
131
+ """
132
+ _enforce_legacy_config_gate()
41
133
  _print_config_deprecation()
134
+ # Emit telemetry for legacy command invocation
135
+ _emit_telemetry_event(
136
+ "config.command",
137
+ {
138
+ "phase": "hard_deprecation",
139
+ "gated_by_env": _is_legacy_config_enabled(),
140
+ },
141
+ )
42
142
 
43
143
 
44
144
  @config_group.command("list")
@@ -49,6 +149,7 @@ def list_config(ctx: click.Context, output_json: bool) -> None:
49
149
 
50
150
  Deprecated: run 'aip accounts list' for profile-aware output.
51
151
  """
152
+ _enforce_legacy_config_gate()
52
153
  console.print(f"[{WARNING_STYLE}]Deprecated: run 'aip accounts list' for profile-aware output.[/]")
53
154
 
54
155
  # Delegate to accounts list by invoking the command
@@ -124,7 +225,10 @@ def set_config(key: str, value: str, account_name: str | None) -> None:
124
225
 
125
226
  For api_url and api_key, this operates on the specified account (or active account).
126
227
  Other keys (timeout, history_default_limit) are global settings.
228
+
229
+ Deprecated: use 'aip accounts edit <name>' instead.
127
230
  """
231
+ _enforce_legacy_config_gate()
128
232
  # For other keys, use legacy config
129
233
  valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
130
234
  if key not in valid_keys:
@@ -169,7 +273,11 @@ def set_config(key: str, value: str, account_name: str | None) -> None:
169
273
  @config_group.command("get")
170
274
  @click.argument("key")
171
275
  def get_config(key: str) -> None:
172
- """Get a configuration value."""
276
+ """Get a configuration value.
277
+
278
+ Deprecated: use 'aip accounts show <name>' or read ~/.aip/config.yaml directly.
279
+ """
280
+ _enforce_legacy_config_gate()
173
281
  config = load_config()
174
282
 
175
283
  value = config.get(key)
@@ -194,7 +302,11 @@ def get_config(key: str) -> None:
194
302
  @config_group.command("unset")
195
303
  @click.argument("key")
196
304
  def unset_config(key: str) -> None:
197
- """Remove a configuration value."""
305
+ """Remove a configuration value.
306
+
307
+ Deprecated: use 'aip accounts edit <name>' to clear specific fields.
308
+ """
309
+ _enforce_legacy_config_gate()
198
310
  config = load_config()
199
311
 
200
312
  if key not in config:
@@ -210,7 +322,11 @@ def unset_config(key: str) -> None:
210
322
  @config_group.command("reset")
211
323
  @click.option("--force", is_flag=True, help="Skip confirmation prompt")
212
324
  def reset_config(force: bool) -> None:
213
- """Reset all configuration to defaults."""
325
+ """Reset all configuration to defaults.
326
+
327
+ Deprecated: use 'aip accounts remove <name>' for each account or manually edit ~/.aip/config.yaml.
328
+ """
329
+ _enforce_legacy_config_gate()
214
330
  if not force:
215
331
  console.print(f"[{WARNING_STYLE}]This will remove all AIP configuration.[/]")
216
332
  confirm = input("Are you sure? (y/N): ").strip().lower()
@@ -235,7 +351,7 @@ def reset_config(force: bool) -> None:
235
351
  # In-memory configuration (e.g., tests) needs explicit clearing
236
352
  save_config({})
237
353
 
238
- hint = command_hint("config configure", slash_command="login")
354
+ hint = command_hint(CONFIG_CONFIGURE_HINT, slash_command="login")
239
355
  message = Text("✅ Configuration reset.", style=SUCCESS_STYLE)
240
356
  if hint:
241
357
  message.append(f" Run '{hint}' to set up again.")
@@ -271,6 +387,396 @@ def _configure_interactive(account_name: str | None = None) -> None:
271
387
  _print_active_account_footer(store)
272
388
 
273
389
 
390
+ @config_group.command("audit")
391
+ @click.option(
392
+ "--path",
393
+ "paths",
394
+ multiple=True,
395
+ help="Glob pattern(s) to search (repeatable). Defaults to current directory.",
396
+ )
397
+ @click.option(
398
+ "--stdin",
399
+ "read_from_stdin",
400
+ is_flag=True,
401
+ help="Read file list from stdin (one path per line).",
402
+ )
403
+ @click.option(
404
+ "--no-gitignore",
405
+ is_flag=True,
406
+ help="Disable .gitignore filtering (default: respects .gitignore).",
407
+ )
408
+ @click.option(
409
+ "--json",
410
+ "output_json",
411
+ is_flag=True,
412
+ help="Output results in JSON format.",
413
+ )
414
+ @click.option(
415
+ "--fail-on-hit/--no-fail-on-hit",
416
+ default=True,
417
+ help="Exit with code 1 if hits are found (default: fail on hit).",
418
+ )
419
+ @click.option(
420
+ "--silent",
421
+ is_flag=True,
422
+ help="Suppress Rich table output when --json is used.",
423
+ )
424
+ def audit_config(
425
+ paths: tuple[str, ...],
426
+ read_from_stdin: bool,
427
+ no_gitignore: bool,
428
+ output_json: bool,
429
+ fail_on_hit: bool,
430
+ silent: bool,
431
+ ) -> None:
432
+ """Scan scripts/configs for deprecated 'aip config' command usage.
433
+
434
+ Finds strings matching 'aip config' (including variations like 'aip-config',
435
+ 'python -m glaip_sdk.cli config') in scripts, CI manifests, and docs.
436
+
437
+ Examples:
438
+ aip config audit
439
+ aip config audit --path "**/*.sh" --path "**/*.yml"
440
+ aip config audit --stdin < file_list.txt
441
+ aip config audit --json --no-fail-on-hit
442
+ """
443
+ _enforce_legacy_config_gate()
444
+ # Collect files to scan
445
+ files_to_scan = _collect_files_to_scan(paths, read_from_stdin)
446
+
447
+ # Filter by gitignore if enabled
448
+ files_to_scan = _filter_by_gitignore(files_to_scan, no_gitignore)
449
+
450
+ # Scan files for matches
451
+ hits = _scan_files_for_matches(files_to_scan)
452
+
453
+ # Emit telemetry
454
+ _emit_telemetry_event(
455
+ "config.audit",
456
+ {
457
+ "audit_invoked": True,
458
+ "hits_found": len(hits),
459
+ "files_scanned": len(files_to_scan),
460
+ },
461
+ )
462
+
463
+ # Output results
464
+ _output_audit_results(hits, len(files_to_scan), output_json, silent)
465
+
466
+ # Exit with appropriate code
467
+ if hits and fail_on_hit:
468
+ sys.exit(1)
469
+ sys.exit(0)
470
+
471
+
472
+ # Patterns to match deprecated config command usage
473
+ _AUDIT_PATTERNS = [
474
+ r"aip\s+config",
475
+ r"aip-config",
476
+ r"python\s+-m\s+glaip_sdk\.cli\s+config",
477
+ r"python\s+-m\s+glaip_sdk\.cli\.main\s+config",
478
+ ]
479
+ _COMPILED_AUDIT_PATTERNS = [re.compile(pattern, re.IGNORECASE) for pattern in _AUDIT_PATTERNS]
480
+
481
+
482
+ def _collect_files_from_stdin() -> list[Path]:
483
+ """Collect files to scan from stdin input.
484
+
485
+ Returns:
486
+ List of file paths read from stdin.
487
+ """
488
+ files_to_scan: list[Path] = []
489
+ for line in sys.stdin:
490
+ line = line.strip()
491
+ if line:
492
+ try:
493
+ file_path = Path(line).expanduser().resolve()
494
+ except Exception:
495
+ continue
496
+ if file_path.exists() and file_path.is_file():
497
+ if _should_skip_file(file_path):
498
+ continue
499
+ files_to_scan.append(file_path)
500
+ return files_to_scan
501
+
502
+
503
+ def _collect_files_from_patterns(paths: tuple[str, ...]) -> list[Path]:
504
+ """Collect files to scan from glob patterns.
505
+
506
+ Args:
507
+ paths: Glob patterns to search.
508
+
509
+ Returns:
510
+ List of file paths matching the patterns.
511
+ """
512
+ files_to_scan: list[Path] = []
513
+ for pattern in paths:
514
+ for file_path in Path.cwd().rglob(pattern):
515
+ if file_path.is_file() and not _should_skip_file(file_path):
516
+ files_to_scan.append(file_path)
517
+ return files_to_scan
518
+
519
+
520
+ def _collect_files_default() -> list[Path]:
521
+ """Collect all files from current directory recursively.
522
+
523
+ Returns:
524
+ List of all file paths in current directory.
525
+ """
526
+ files_to_scan: list[Path] = []
527
+ base = Path.cwd()
528
+ max_files = _resolve_audit_max_files()
529
+
530
+ for root, dirs, files in os.walk(base):
531
+ dirs[:] = [d for d in dirs if d not in _DEFAULT_EXCLUDE_DIRS]
532
+ for file in files:
533
+ file_path = Path(root) / file
534
+ if _should_skip_file(file_path):
535
+ continue
536
+ files_to_scan.append(file_path)
537
+ if max_files and len(files_to_scan) >= max_files:
538
+ _warn_scan_truncated(max_files)
539
+ return files_to_scan
540
+
541
+ return files_to_scan
542
+
543
+
544
+ def _resolve_audit_max_files() -> int | None:
545
+ """Resolve optional scan limit from env."""
546
+ max_files_env = os.getenv("AIP_CONFIG_AUDIT_MAX_FILES")
547
+ if not max_files_env:
548
+ return None
549
+ try:
550
+ parsed = int(max_files_env, 10)
551
+ except ValueError:
552
+ return None
553
+ return parsed if parsed > 0 else None
554
+
555
+
556
+ def _warn_scan_truncated(max_files: int) -> None:
557
+ """Warn when scanning is truncated to avoid surprises on huge repos."""
558
+ console.print(
559
+ f"[{WARNING_STYLE}]Scanning limited to the first {max_files} files. "
560
+ "Use --path to narrow the search or increase AIP_CONFIG_AUDIT_MAX_FILES to scan more.[/]"
561
+ )
562
+
563
+
564
+ def _collect_files_to_scan(paths: tuple[str, ...], read_from_stdin: bool) -> list[Path]:
565
+ """Collect files to scan based on input method.
566
+
567
+ Args:
568
+ paths: Glob patterns to search (if not reading from stdin).
569
+ read_from_stdin: Whether to read file list from stdin.
570
+
571
+ Returns:
572
+ List of file paths to scan.
573
+ """
574
+ if read_from_stdin:
575
+ return _collect_files_from_stdin()
576
+ if paths:
577
+ return _collect_files_from_patterns(paths)
578
+ return _collect_files_default()
579
+
580
+
581
+ def _filter_by_gitignore(files_to_scan: list[Path], no_gitignore: bool) -> list[Path]:
582
+ """Filter files by .gitignore patterns if enabled.
583
+
584
+ Args:
585
+ files_to_scan: List of file paths to filter.
586
+ no_gitignore: If True, skip gitignore filtering.
587
+
588
+ Returns:
589
+ Filtered list of file paths.
590
+ """
591
+ global _PATHSPEC_WARNED
592
+ if no_gitignore or pathspec is None:
593
+ if not no_gitignore and pathspec is None and not _PATHSPEC_WARNED:
594
+ msg = (
595
+ f"[{WARNING_STYLE}]Warning:[/] pathspec is not installed; "
596
+ "gitignore filtering for 'aip config audit' will be skipped."
597
+ )
598
+ with _PATHSPEC_WARNED_LOCK:
599
+ if not _PATHSPEC_WARNED:
600
+ stderr_console.print(msg)
601
+ _PATHSPEC_WARNED = True
602
+ return files_to_scan
603
+
604
+ # Load .gitignore patterns
605
+ gitignore_path = Path.cwd() / ".gitignore"
606
+ if not gitignore_path.exists():
607
+ return files_to_scan
608
+
609
+ try:
610
+ with gitignore_path.open(encoding="utf-8", errors="ignore") as f:
611
+ spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
612
+
613
+ # Guard against files outside CWD; fallback to absolute path in that case
614
+ def _to_git_path(path: Path) -> str:
615
+ try:
616
+ return str(path.relative_to(Path.cwd()))
617
+ except ValueError:
618
+ return str(path)
619
+
620
+ return [path for path in files_to_scan if not spec.match_file(_to_git_path(path))]
621
+ except Exception:
622
+ # If gitignore parsing fails, return all files
623
+ return files_to_scan
624
+
625
+
626
+ def _should_skip_file(file_path: Path) -> bool:
627
+ """Check whether a file should be skipped based on size."""
628
+ try:
629
+ return file_path.stat().st_size > _MAX_SCAN_FILE_SIZE
630
+ except OSError:
631
+ return False
632
+
633
+
634
+ def _extract_match_snippet(line: str, match_obj: re.Match[str]) -> str:
635
+ """Extract a snippet around a match for display.
636
+
637
+ Args:
638
+ line: The full line containing the match.
639
+ match_obj: The regex match object.
640
+
641
+ Returns:
642
+ A snippet of text around the match.
643
+ """
644
+ start = max(0, match_obj.start() - 20)
645
+ end = min(len(line), match_obj.end() + 20)
646
+ return line[start:end].strip()
647
+
648
+
649
+ def _process_file_for_matches(file_path: Path, compiled_patterns: list[re.Pattern[str]]) -> list[dict[str, Any]]:
650
+ """Process a single file for deprecated config command matches.
651
+
652
+ Args:
653
+ file_path: Path to the file to scan.
654
+ compiled_patterns: List of compiled regex patterns to match.
655
+
656
+ Returns:
657
+ List of hit dictionaries found in this file.
658
+ """
659
+ hits: list[dict[str, Any]] = []
660
+ if _should_skip_file(file_path):
661
+ return hits
662
+ try:
663
+ with file_path.open(encoding="utf-8", errors="ignore") as f:
664
+ for line_num, line in enumerate(f, start=1):
665
+ for pattern in compiled_patterns:
666
+ match_obj = pattern.search(line)
667
+ if match_obj:
668
+ snippet = _extract_match_snippet(line, match_obj)
669
+ replacement = _suggest_replacement(line.strip())
670
+
671
+ try:
672
+ file_str = str(file_path.relative_to(Path.cwd()))
673
+ except ValueError:
674
+ file_str = str(file_path)
675
+
676
+ hits.append(
677
+ {
678
+ "file": file_str,
679
+ "line": line_num,
680
+ "match": snippet,
681
+ "replacement": replacement,
682
+ }
683
+ )
684
+ break # Only count once per line
685
+ except (UnicodeDecodeError, PermissionError):
686
+ # Skip binary files or files we can't read
687
+ pass
688
+ except OSError:
689
+ # Skip files with permission errors
690
+ pass
691
+
692
+ return hits
693
+
694
+
695
+ def _scan_files_for_matches(files_to_scan: list[Path]) -> list[dict[str, Any]]:
696
+ """Scan files for deprecated config command usage.
697
+
698
+ Args:
699
+ files_to_scan: List of file paths to scan.
700
+
701
+ Returns:
702
+ List of hit dictionaries with file, line, match, and replacement info.
703
+ """
704
+ hits: list[dict[str, Any]] = []
705
+
706
+ for file_path in files_to_scan:
707
+ file_hits = _process_file_for_matches(file_path, _COMPILED_AUDIT_PATTERNS)
708
+ hits.extend(file_hits)
709
+
710
+ return hits
711
+
712
+
713
+ def _output_audit_results(hits: list[dict[str, Any]], files_scanned: int, output_json: bool, silent: bool) -> None:
714
+ """Output audit results in the requested format.
715
+
716
+ Args:
717
+ hits: List of hit dictionaries.
718
+ files_scanned: Number of files scanned.
719
+ output_json: If True, output JSON format.
720
+ silent: If True, suppress Rich output when using JSON.
721
+ """
722
+ if output_json:
723
+ result = {
724
+ "hits": hits,
725
+ "total_hits": len(hits),
726
+ "files_scanned": files_scanned,
727
+ }
728
+ click.echo(json.dumps(result, indent=2))
729
+ return
730
+
731
+ if silent:
732
+ return
733
+
734
+ if hits:
735
+ table = AIPTable(title="⚠️ Deprecated 'aip config' Usage Found")
736
+ table.add_column("File", style=INFO, width=30)
737
+ table.add_column("Line", style=NEUTRAL, width=8)
738
+ table.add_column("Match", style=WARNING_STYLE, width=40)
739
+ table.add_column("Suggested Replacement", style=SUCCESS_STYLE, width=40)
740
+
741
+ for hit in hits:
742
+ table.add_row(
743
+ hit["file"],
744
+ str(hit["line"]),
745
+ hit["match"],
746
+ hit["replacement"],
747
+ )
748
+
749
+ console.print(table)
750
+ console.print(f"\n[{WARNING_STYLE}]Found {len(hits)} deprecated usage(s).[/]")
751
+ else:
752
+ console.print(f"[{SUCCESS_STYLE}]✅ No deprecated 'aip config' usage found.[/]")
753
+
754
+
755
+ def _suggest_replacement(line: str) -> str:
756
+ """Suggest a replacement command for deprecated config usage."""
757
+ line_lower = line.lower()
758
+
759
+ # Map common patterns to replacements
760
+ if "config list" in line_lower:
761
+ return "aip accounts list"
762
+ elif "config set" in line_lower:
763
+ if "api_url" in line_lower or "api_key" in line_lower:
764
+ return "aip accounts edit <name> [--url URL] [--key]"
765
+ return "aip accounts edit <name>"
766
+ elif "config get" in line_lower:
767
+ return "aip accounts show <name> (or read ~/.aip/config.yaml)"
768
+ elif "config unset" in line_lower:
769
+ return "aip accounts edit <name> (to clear specific fields)"
770
+ elif "config reset" in line_lower:
771
+ return "aip accounts remove <name> (for each account)"
772
+ # Generic "config" usage (command-like), but avoid matching any arbitrary
773
+ # mention of the word "config" in unrelated text.
774
+ elif "aip config" in line_lower or " config " in f" {line_lower} " or CONFIG_CONFIGURE_HINT in line_lower:
775
+ return "aip configure or aip accounts add <name>"
776
+ else:
777
+ return "Use 'aip accounts ...' or 'aip configure'"
778
+
779
+
274
780
  @config_group.command()
275
781
  @click.option(
276
782
  "--account",
@@ -283,6 +789,7 @@ def configure(account_name: str | None) -> None:
283
789
  This command is an alias for 'aip accounts add <name>' and will
284
790
  configure the specified account (or active account if not specified).
285
791
  """
792
+ _enforce_legacy_config_gate()
286
793
  _configure_interactive(account_name)
287
794
 
288
795
 
@@ -299,10 +806,15 @@ def configure_command(account_name: str | None) -> None:
299
806
  This is an alias for 'aip config configure' for backward compatibility.
300
807
  For multi-account support, use 'aip accounts add <name>' instead.
301
808
  """
302
- console.print(
303
- f"[{WARNING_STYLE}]Setup tip:[/] Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for "
304
- "multi-account setup. Launching the interactive wizard now..."
305
- )
809
+ _enforce_legacy_config_gate()
810
+ suppress_tip = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP", "").strip().lower() in {"1", "true", "yes", "on"}
811
+ if not suppress_tip:
812
+ tip_prefix = f"[{WARNING_STYLE}]Setup tip:[/] "
813
+ tip_body = (
814
+ "Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for multi-account setup. "
815
+ "Launching the interactive wizard now..."
816
+ )
817
+ console.print(f"{tip_prefix}{tip_body}")
306
818
  # Delegate to the shared function
307
819
  _configure_interactive(account_name)
308
820
 
@@ -311,28 +823,6 @@ def configure_command(account_name: str | None) -> None:
311
823
  _mask_api_key = mask_api_key_display
312
824
 
313
825
 
314
- def _print_missing_config_hint() -> None:
315
- """Show guidance when no configuration file exists."""
316
- hint = command_hint("config configure", slash_command="login")
317
- if hint:
318
- console.print(f"[{WARNING_STYLE}]No configuration found.[/] Run {format_command_hint(hint) or hint} to set up.")
319
- else:
320
- console.print(f"[{WARNING_STYLE}]No configuration found.[/]")
321
-
322
-
323
- def _render_config_table(config: dict[str, str]) -> None:
324
- """Render the current configuration in a friendly table."""
325
- table = AIPTable(title=f"{ICON_TOOL} AIP Configuration")
326
- table.add_column("Setting", style=INFO, width=20)
327
- table.add_column("Value", style=SUCCESS)
328
-
329
- for key, value in config.items():
330
- table.add_row(key, _mask_api_key(value) if key == "api_key" else str(value))
331
-
332
- console.print(table)
333
- console.print(Text(f"\n📁 Config file: {CONFIG_FILE}"))
334
-
335
-
336
826
  def _render_configuration_header() -> None:
337
827
  """Display the interactive configuration heading/banner."""
338
828
  render_branding_header(console, "[bold]AIP Configuration[/bold]")
@@ -279,11 +279,8 @@ class SlashSession:
279
279
  def _ensure_configuration(self) -> bool:
280
280
  """Ensure the CLI has both API URL and credentials before continuing."""
281
281
  while not self._configuration_ready():
282
- self.console.print(
283
- f"[{WARNING_STYLE}]Configuration required.[/] "
284
- "Slash mode cannot run 'aip accounts ...'. Run setup from your terminal (e.g., "
285
- "'aip accounts add default' or 'aip configure'), or continue with the `/login` wizard here..."
286
- )
282
+ previous_tip_env = os.environ.get("AIP_SUPPRESS_CONFIGURE_TIP")
283
+ os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = "1"
287
284
  self._suppress_login_layout = True
288
285
  try:
289
286
  self._cmd_login([], False)
@@ -292,6 +289,10 @@ class SlashSession:
292
289
  return False
293
290
  finally:
294
291
  self._suppress_login_layout = False
292
+ if previous_tip_env is None:
293
+ os.environ.pop("AIP_SUPPRESS_CONFIGURE_TIP", None)
294
+ else:
295
+ os.environ["AIP_SUPPRESS_CONFIGURE_TIP"] = previous_tip_env
295
296
 
296
297
  return True
297
298
 
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import importlib
10
10
  import logging
11
+ import sys
11
12
  from collections.abc import Callable, Iterable, Iterator
12
13
  from contextlib import contextmanager
13
14
  from typing import Any, Literal
@@ -73,6 +74,10 @@ def _fetch_latest_version(package_name: str) -> str | None:
73
74
 
74
75
  def _should_check_for_updates() -> bool:
75
76
  """Return False when update checks are explicitly disabled."""
77
+ # Check module attribute first (for test overrides), then fall back to imported constant
78
+ module = sys.modules.get(__name__)
79
+ if module and hasattr(module, "UPDATE_CHECK_ENABLED"):
80
+ return getattr(module, "UPDATE_CHECK_ENABLED")
76
81
  return UPDATE_CHECK_ENABLED
77
82
 
78
83
 
glaip_sdk/cli/utils.py CHANGED
@@ -875,10 +875,10 @@ def _strip_spaces_for_matching(value: str) -> str:
875
875
  return re.sub(r"\s+", "", value)
876
876
 
877
877
 
878
- def _is_fuzzy_match(search: Any, target: str) -> bool:
879
- """Case-insensitive fuzzy match with optional spaces."""
878
+ def _is_fuzzy_match(search: Any, target: Any) -> bool:
879
+ """Case-insensitive fuzzy match with optional spaces; returns False for non-string inputs."""
880
880
  # Ensure search is a string
881
- if not isinstance(search, str):
881
+ if not isinstance(search, str) or not isinstance(target, str):
882
882
  return False
883
883
 
884
884
  if not search:
@@ -1000,13 +1000,15 @@ def _rank_labels(labels: list[str], query: Any) -> list[str]:
1000
1000
  Labels sorted by fuzzy score (descending), then case-insensitive label,
1001
1001
  then id suffix for deterministic ordering.
1002
1002
  """
1003
+ suffix_cache = {label: _extract_id_suffix(label) for label in labels}
1004
+
1003
1005
  if not query:
1004
1006
  # No query: sort by case-insensitive label, then id suffix
1005
- return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1007
+ return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
1006
1008
 
1007
1009
  # Ensure query is a string
1008
1010
  if not isinstance(query, str):
1009
- return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1011
+ return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
1010
1012
 
1011
1013
  query_lower = query.lower()
1012
1014
 
@@ -1016,11 +1018,11 @@ def _rank_labels(labels: list[str], query: Any) -> list[str]:
1016
1018
  label_lower = label.lower()
1017
1019
  score = _fuzzy_score(query_lower, label_lower)
1018
1020
  if score >= 0: # Only include matches
1019
- scored_labels.append((score, label_lower, _extract_id_suffix(label), label))
1021
+ scored_labels.append((score, label_lower, suffix_cache[label], label))
1020
1022
 
1021
1023
  if not scored_labels:
1022
1024
  # No fuzzy matches: fall back to deterministic label sorting
1023
- return sorted(labels, key=lambda lbl: (lbl.lower(), _extract_id_suffix(lbl)))
1025
+ return sorted(labels, key=lambda lbl: (lbl.lower(), suffix_cache[lbl]))
1024
1026
 
1025
1027
  # Sort by: score (desc), label (case-insensitive), id suffix, original label
1026
1028
  scored_labels.sort(key=lambda x: (-x[0], x[1], x[2], x[3]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
@@ -6,10 +6,10 @@ glaip_sdk/cli/account_store.py,sha256=NXuAVPaJS_32Aw1VTaZCNwIID-gADw4F_UMieoWmg3
6
6
  glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
7
7
  glaip_sdk/cli/auth.py,sha256=9hfjZyd4cx2_mImqykJ1sWQsuVTR2gy6D4hFqAQNKL4,24129
8
8
  glaip_sdk/cli/commands/__init__.py,sha256=6Z3ASXDut0lAbUX_umBFtxPzzFyqoiZfVeTahThFu1A,219
9
- glaip_sdk/cli/commands/accounts.py,sha256=VCG-JZGY86DlWO5bAfDZ70RuyKQ5q-Rh4U0iM8NwO6M,13755
10
- glaip_sdk/cli/commands/agents.py,sha256=y89okY-a5sM_QCS3F3C66DF7yhhHFbUJ7ZzIl2DUEck,47880
9
+ glaip_sdk/cli/commands/accounts.py,sha256=B5itsUzqoH_hBRYOVd2m4nPoIuBbPDIoK974zKMm9NE,18635
10
+ glaip_sdk/cli/commands/agents.py,sha256=WCOzllyh_Znwlju5camT4vE6OeRJbsAmjWwcyiAqWs4,48429
11
11
  glaip_sdk/cli/commands/common_config.py,sha256=IY13gPkeifXxSdpzRFUvfRin8J7s38p6Y7TYjdGw7w4,2474
12
- glaip_sdk/cli/commands/configure.py,sha256=8vfgtNEMK2lnEk3i6H1ZevsjxnYA6jAj4evhWmsHi6w,14494
12
+ glaip_sdk/cli/commands/configure.py,sha256=95PQiJnpvsdH02v_tLVANd64qAJJnZKlhNe4tpfWIS4,30262
13
13
  glaip_sdk/cli/commands/mcps.py,sha256=tttqQnfM89iI9Pm94u8YRhiHMQNYNouecFX0brsT4cQ,42551
14
14
  glaip_sdk/cli/commands/models.py,sha256=vfcGprK5CHprQ0CNpNzQlNNTELvdgKC7JxTG_ijOwmE,2009
15
15
  glaip_sdk/cli/commands/tools.py,sha256=7_RMTuTI1Guu7psClovbyt2umfk4rkp7jSW19GXKA44,18440
@@ -33,7 +33,7 @@ glaip_sdk/cli/slash/__init__.py,sha256=J9TPL2UcNTkW8eifG6nRmAEGHhyEgdYMYk4cHaaOb
33
33
  glaip_sdk/cli/slash/agent_session.py,sha256=9r1xNRk5mk6rfJXV6KIf2Yo4B4hjknimd9fkxH1LO3c,11304
34
34
  glaip_sdk/cli/slash/prompt.py,sha256=2urqR3QqN3O09lHmKKSEbhsIdlS4B7hm9O8AP_VwCSU,8034
35
35
  glaip_sdk/cli/slash/remote_runs_controller.py,sha256=Ok6CezIeF1CPGQ8-QN3TRx5kGGEACOrgyPwH_BRRCyI,21354
36
- glaip_sdk/cli/slash/session.py,sha256=GvdpLri_TzuaWDEWHdfAIbdOjMf7MUGewxpEtW_5hPk,57642
36
+ glaip_sdk/cli/slash/session.py,sha256=Pl6au6zwiKYeJ1JyxQ3pVfwVrE0Lzi6ejlLtEJfYG_4,57677
37
37
  glaip_sdk/cli/slash/tui/__init__.py,sha256=ljBAeAFY2qNDkbJrZh5NgXxjwUlsv9-UxgKNIv0AF1Q,274
38
38
  glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=YAtBtgjtzIy5y2NVOQexZX783DJpqFUkwAVYkVn1tSo,24762
39
39
  glaip_sdk/cli/transcript/__init__.py,sha256=yiYHyNtebMCu3BXu56Xm5RBC2tDc865q8UGPnoe6QRs,920
@@ -43,8 +43,8 @@ glaip_sdk/cli/transcript/export.py,sha256=reCvrZVzli8_LzYe5ZNdaa-MwZ1ov2RjnDzKZW
43
43
  glaip_sdk/cli/transcript/history.py,sha256=2FBjawxP8CX9gRPMUMP8bDjG50BGM2j2zk6IfHvAMH4,26211
44
44
  glaip_sdk/cli/transcript/launcher.py,sha256=z5ivkPXDQJpATIqtRLUK8jH3p3WIZ72PvOPqYRDMJvw,2327
45
45
  glaip_sdk/cli/transcript/viewer.py,sha256=ar1SzRkhKIf3_DgFz1EG1RZGDmd2w2wogAe038DLL_M,13037
46
- glaip_sdk/cli/update_notifier.py,sha256=qv-GfwTYZdrhlSbC_71I1AvKY9cV4QVBmtees16S2Xg,9807
47
- glaip_sdk/cli/utils.py,sha256=iPtt4xAqtCW-dwQ-JWVwoPVPAm-P1R8C-1kih6ZIYXU,57255
46
+ glaip_sdk/cli/update_notifier.py,sha256=FnTjzS8YT94RmP6c5aU_XNIyRi7FRHvAskMy-VJikl8,10064
47
+ glaip_sdk/cli/utils.py,sha256=fV6PZlQ7K5zckpFWvwh3yLmETGrVylK9AXtN7zKBp-A,57374
48
48
  glaip_sdk/cli/validators.py,sha256=d-kq4y7HWMo6Gc7wLXWUsCt8JwFvJX_roZqRm1Nko1I,5622
49
49
  glaip_sdk/client/__init__.py,sha256=F-eE_dRSzA0cc1it06oi0tZetZBHmSUjWSHGhJMLCls,263
50
50
  glaip_sdk/client/_agent_payloads.py,sha256=VfBHoijuoqUOixGBf2SA2vlQIXQmBsjB3sXHZhXYiec,17681
@@ -107,7 +107,7 @@ glaip_sdk/utils/resource_refs.py,sha256=vF34kyAtFBLnaKnQVrsr2st1JiSxVbIZ4yq0DelJ
107
107
  glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
108
108
  glaip_sdk/utils/serialization.py,sha256=z-qpvWLSBrGK3wbUclcA1UIKLXJedTnMSwPdq-FF4lo,13308
109
109
  glaip_sdk/utils/validation.py,sha256=Vt8oSnn7OM6ns5vjOl5FwGIMWBPb0yI6RD5XL_L5_4M,6826
110
- glaip_sdk-0.5.1.dist-info/METADATA,sha256=jxEyfPZqz2g7nLHnFidlNnMPkljgrLyKVYk3qVnThLE,7053
111
- glaip_sdk-0.5.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
112
- glaip_sdk-0.5.1.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
113
- glaip_sdk-0.5.1.dist-info/RECORD,,
110
+ glaip_sdk-0.5.3.dist-info/METADATA,sha256=RwJBNhUEnTWDIPr-X4oYiLk-8GtG2IzBoV41jxGkad0,7053
111
+ glaip_sdk-0.5.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
112
+ glaip_sdk-0.5.3.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
113
+ glaip_sdk-0.5.3.dist-info/RECORD,,