stkai 0.2.5__tar.gz → 0.3.2__tar.gz

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 (29) hide show
  1. {stkai-0.2.5/src/stkai.egg-info → stkai-0.3.2}/PKG-INFO +1 -1
  2. {stkai-0.2.5 → stkai-0.3.2}/pyproject.toml +1 -1
  3. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/__init__.py +4 -0
  4. stkai-0.3.2/src/stkai/_cli.py +53 -0
  5. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_config.py +467 -17
  6. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_http.py +11 -5
  7. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_remote_quick_command.py +10 -11
  8. {stkai-0.2.5 → stkai-0.3.2/src/stkai.egg-info}/PKG-INFO +1 -1
  9. {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/SOURCES.txt +2 -0
  10. stkai-0.3.2/tests/test_cli.py +82 -0
  11. stkai-0.3.2/tests/test_config.py +1117 -0
  12. {stkai-0.2.5 → stkai-0.3.2}/tests/test_http.py +2 -0
  13. stkai-0.2.5/tests/test_config.py +0 -617
  14. {stkai-0.2.5 → stkai-0.3.2}/LICENSE +0 -0
  15. {stkai-0.2.5 → stkai-0.3.2}/README.md +0 -0
  16. {stkai-0.2.5 → stkai-0.3.2}/setup.cfg +0 -0
  17. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_auth.py +0 -0
  18. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/_utils.py +0 -0
  19. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/agents/__init__.py +0 -0
  20. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/agents/_agent.py +0 -0
  21. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/agents/_models.py +0 -0
  22. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/__init__.py +0 -0
  23. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_event_listeners.py +0 -0
  24. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_handlers.py +0 -0
  25. {stkai-0.2.5 → stkai-0.3.2}/src/stkai/rqc/_models.py +0 -0
  26. {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/dependency_links.txt +0 -0
  27. {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/requires.txt +0 -0
  28. {stkai-0.2.5 → stkai-0.3.2}/src/stkai.egg-info/top_level.txt +0 -0
  29. {stkai-0.2.5 → stkai-0.3.2}/tests/test_auth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stkai
3
- Version: 0.2.5
3
+ Version: 0.3.2
4
4
  Summary: Python SDK for StackSpot AI - Remote Quick Commands and more
5
5
  Author-email: Rafael Ponte <rponte@gmail.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stkai"
7
- version = "0.2.5"
7
+ version = "0.3.2"
8
8
  description = "Python SDK for StackSpot AI - Remote Quick Commands and more"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -80,9 +80,11 @@ from stkai._config import (
80
80
  STKAI,
81
81
  AgentConfig,
82
82
  AuthConfig,
83
+ ConfigEntry,
83
84
  RateLimitConfig,
84
85
  RateLimitStrategy,
85
86
  RqcConfig,
87
+ SdkConfig,
86
88
  STKAIConfig,
87
89
  )
88
90
  from stkai._http import (
@@ -113,6 +115,8 @@ __all__ = [
113
115
  # Configuration
114
116
  "STKAI",
115
117
  "STKAIConfig",
118
+ "SdkConfig",
119
+ "ConfigEntry",
116
120
  "AuthConfig",
117
121
  "RqcConfig",
118
122
  "AgentConfig",
@@ -0,0 +1,53 @@
1
+ """
2
+ StackSpot CLI (oscli) integration.
3
+
4
+ This module provides an abstraction layer for interacting with the StackSpot CLI,
5
+ allowing the SDK to detect CLI mode and retrieve CLI-specific configuration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class StkCLI:
12
+ """
13
+ Abstraction for StackSpot CLI (oscli) integration.
14
+
15
+ Provides methods to:
16
+ - Check if running in CLI mode (oscli available)
17
+ - Retrieve CLI-specific configuration values
18
+
19
+ Example:
20
+ >>> if StkCLI.is_available():
21
+ ... base_url = StkCLI.get_codebuddy_base_url()
22
+ """
23
+
24
+ @staticmethod
25
+ def is_available() -> bool:
26
+ """
27
+ Check if StackSpot CLI (oscli) is available.
28
+
29
+ Returns:
30
+ True if oscli can be imported (CLI mode), False otherwise.
31
+ """
32
+ try:
33
+ import oscli # noqa: F401
34
+
35
+ return True
36
+ except ImportError:
37
+ return False
38
+
39
+ @staticmethod
40
+ def get_codebuddy_base_url() -> str | None:
41
+ """
42
+ Get CodeBuddy base URL from CLI if available.
43
+
44
+ Returns:
45
+ The CLI's __codebuddy_base_url__ if oscli is installed
46
+ and the attribute exists, None otherwise.
47
+ """
48
+ try:
49
+ from oscli import __codebuddy_base_url__
50
+
51
+ return __codebuddy_base_url__ if __codebuddy_base_url__ else None
52
+ except (ImportError, AttributeError):
53
+ return None
@@ -7,9 +7,10 @@ If not called, sensible defaults are used.
7
7
 
8
8
  Hierarchy of precedence (highest to lowest):
9
9
  1. *Options passed to client constructors
10
- 2. Environment variables (STKAI_*) - when allow_env_override=True
11
- 3. Values set via STKAI.configure()
12
- 4. Hardcoded defaults (in dataclass fields)
10
+ 2. Values set via STKAI.configure()
11
+ 3. StackSpot CLI values (oscli) - if CLI is available
12
+ 4. Environment variables (STKAI_*) - when allow_env_override=True
13
+ 5. Hardcoded defaults (in dataclass fields)
13
14
 
14
15
  Example:
15
16
  >>> from stkai import STKAI
@@ -27,7 +28,9 @@ Example:
27
28
  from __future__ import annotations
28
29
 
29
30
  import os
31
+ from collections.abc import Callable
30
32
  from dataclasses import dataclass, field, fields, replace
33
+ from functools import wraps
31
34
  from typing import Any, Literal, Self
32
35
 
33
36
  # Type alias for rate limit strategies
@@ -100,6 +103,46 @@ class OverridableConfig:
100
103
  # =============================================================================
101
104
 
102
105
 
106
+ @dataclass(frozen=True)
107
+ class SdkConfig:
108
+ """
109
+ SDK metadata (read-only, not configurable).
110
+
111
+ Provides information about the SDK version and runtime environment.
112
+ These values are automatically detected and cannot be overridden.
113
+
114
+ Attributes:
115
+ version: The installed SDK version.
116
+ cli_mode: Whether StackSpot CLI (oscli) is available.
117
+
118
+ Example:
119
+ >>> from stkai import STKAI
120
+ >>> STKAI.config.sdk.version
121
+ '0.2.8'
122
+ >>> STKAI.config.sdk.cli_mode
123
+ True
124
+ """
125
+
126
+ version: str
127
+ cli_mode: bool
128
+
129
+ @classmethod
130
+ def detect(cls) -> SdkConfig:
131
+ """
132
+ Detect SDK metadata from the runtime environment.
133
+
134
+ Returns:
135
+ SdkConfig with version and cli_mode auto-detected.
136
+ """
137
+ from stkai import __version__
138
+ from stkai._cli import StkCLI
139
+
140
+ return cls(
141
+ version=__version__,
142
+ cli_mode=StkCLI.is_available(),
143
+ )
144
+
145
+
103
146
  @dataclass(frozen=True)
104
147
  class AuthConfig(OverridableConfig):
105
148
  """
@@ -330,15 +373,244 @@ class RateLimitConfig(OverridableConfig):
330
373
  return super().with_overrides(processed, allow_none_fields=merged_allow_none)
331
374
 
332
375
 
376
+ @dataclass(frozen=True)
377
+ class ConfigEntry:
378
+ """
379
+ A configuration field with its resolved value and source.
380
+
381
+ Represents a single configuration entry with metadata about where
382
+ the value came from. Used by explain_data() for structured output.
383
+
384
+ Attributes:
385
+ name: The field name (e.g., "request_timeout").
386
+ value: The resolved value.
387
+ source: Where the value came from:
388
+ - "default": Hardcoded default value
389
+ - "env:VAR_NAME": Environment variable
390
+ - "CLI": StackSpot CLI (oscli)
391
+ - "configure": Set via STKAI.configure()
392
+
393
+ Example:
394
+ >>> entry = ConfigEntry("request_timeout", 60, "configure")
395
+ >>> entry.name
396
+ 'request_timeout'
397
+ >>> entry.value
398
+ 60
399
+ >>> entry.source
400
+ 'configure'
401
+ >>> entry.formatted_value
402
+ '60'
403
+ """
404
+
405
+ name: str
406
+ value: Any
407
+ source: str
408
+
409
+ @property
410
+ def formatted_value(self) -> str:
411
+ """
412
+ Return value formatted for display.
413
+
414
+ Masks sensitive fields (e.g., client_secret) showing only
415
+ first and last 4 characters, and truncates long strings.
416
+
417
+ Returns:
418
+ Formatted string representation of the value.
419
+
420
+ Examples:
421
+ >>> ConfigEntry("client_secret", "super-secret-key", "configure").formatted_value
422
+ 'supe********-key'
423
+ >>> ConfigEntry("client_secret", "short", "configure").formatted_value
424
+ '********t'
425
+ """
426
+ # Mask sensitive fields
427
+ if self.name in ("client_secret",) and self.value is not None:
428
+ secret = str(self.value)
429
+ # Long secrets: show first 4 and last 4 chars
430
+ if len(secret) >= 12:
431
+ return f"{secret[:4]}********{secret[-4:]}"
432
+ # Short secrets: show last 1/3 of chars
433
+ if len(secret) >= 3:
434
+ visible = max(1, len(secret) // 3)
435
+ return f"********{secret[-visible:]}"
436
+ return "********"
437
+
438
+ # Handle None
439
+ if self.value is None:
440
+ return "None"
441
+
442
+ # Convert to string and truncate if needed
443
+ str_value = str(self.value)
444
+ max_length = 50
445
+ if len(str_value) > max_length:
446
+ return str_value[: max_length - 3] + "..."
447
+
448
+ return str_value
449
+
450
+
451
+ @dataclass(frozen=True)
452
+ class STKAIConfigTracker:
453
+ """
454
+ Tracks the source of config field values.
455
+
456
+ An immutable tracker that records where each configuration value came from
457
+ (default, env var, CLI, or configure()). Used internally by STKAIConfig
458
+ for debugging via STKAI.explain().
459
+
460
+ Attributes:
461
+ sources: Dict tracking source of each field value.
462
+ Structure: {"section": {"field": "source"}}
463
+ Source values: "default", "env:VAR_NAME", "CLI", "configure"
464
+
465
+ Example:
466
+ >>> tracker = STKAIConfigTracker()
467
+ >>> tracker = tracker.with_changes_tracked(old_cfg, new_cfg, "env")
468
+ >>> tracker.sources.get("rqc", {}).get("request_timeout")
469
+ 'env:STKAI_RQC_REQUEST_TIMEOUT'
470
+ """
471
+
472
+ sources: dict[str, dict[str, str]] = field(default_factory=dict)
473
+
474
+ @staticmethod
475
+ def track_changes(
476
+ source_type: str,
477
+ ) -> Callable[[Callable[..., STKAIConfig]], Callable[..., STKAIConfig]]:
478
+ """
479
+ Decorator that tracks config changes made by the decorated method.
480
+
481
+ Wraps methods that return a new STKAIConfig, automatically detecting
482
+ changes between the original config (self) and the returned config,
483
+ then recording those changes in the tracker.
484
+
485
+ Args:
486
+ source_type: Source label for tracking ("env", "CLI", or "configure").
487
+
488
+ Returns:
489
+ Decorator function that wraps the method.
490
+
491
+ Example:
492
+ >>> @STKAIConfigTracker.track_changes("env")
493
+ ... def with_env_vars(self) -> STKAIConfig:
494
+ ... return STKAIConfig(...)
495
+ """
496
+
497
+ def decorator(
498
+ method: Callable[..., STKAIConfig],
499
+ ) -> Callable[..., STKAIConfig]:
500
+ @wraps(method)
501
+ def wrapper(self: STKAIConfig, *args: Any, **kwargs: Any) -> STKAIConfig:
502
+ new_config = method(self, *args, **kwargs)
503
+ new_tracker = self._tracker.with_changes_tracked(
504
+ self, new_config, source_type
505
+ )
506
+ return replace(new_config, _tracker=new_tracker)
507
+
508
+ return wrapper
509
+
510
+ return decorator
511
+
512
+ def with_changes_tracked(
513
+ self,
514
+ old_config: STKAIConfig,
515
+ new_config: STKAIConfig,
516
+ source_type: str,
517
+ ) -> STKAIConfigTracker:
518
+ """
519
+ Return new tracker with changes between configs tracked.
520
+
521
+ Compares old and new configs, detects changed fields, and returns
522
+ a new tracker with those changes recorded.
523
+
524
+ Args:
525
+ old_config: The config before changes.
526
+ new_config: The config after changes.
527
+ source_type: Source label ("env", "CLI", or "configure").
528
+
529
+ Returns:
530
+ New STKAIConfigTracker with detected changes tracked.
531
+ """
532
+ changes = self._detect_changes(old_config, new_config)
533
+ new_sources = self._merge_sources(changes, source_type)
534
+ return STKAIConfigTracker(sources=new_sources)
535
+
536
+ def _detect_changes(
537
+ self,
538
+ old_config: STKAIConfig,
539
+ new_config: STKAIConfig,
540
+ ) -> dict[str, list[str]]:
541
+ """
542
+ Detect which fields changed between two configs.
543
+
544
+ Args:
545
+ old_config: The config before changes.
546
+ new_config: The config after changes.
547
+
548
+ Returns:
549
+ Dict mapping section names to lists of changed field names.
550
+ Example: {"rqc": ["request_timeout", "base_url"]}
551
+ """
552
+ changes: dict[str, list[str]] = {}
553
+
554
+ for section_name in ("auth", "rqc", "agent", "rate_limit"):
555
+ old_section = getattr(old_config, section_name)
556
+ new_section = getattr(new_config, section_name)
557
+
558
+ changed_fields = []
559
+ for f in fields(old_section):
560
+ old_val = getattr(old_section, f.name)
561
+ new_val = getattr(new_section, f.name)
562
+ if old_val != new_val:
563
+ changed_fields.append(f.name)
564
+
565
+ if changed_fields:
566
+ changes[section_name] = changed_fields
567
+
568
+ return changes
569
+
570
+ def _merge_sources(
571
+ self,
572
+ changes: dict[str, list[str]],
573
+ source_type: str,
574
+ ) -> dict[str, dict[str, str]]:
575
+ """
576
+ Merge detected changes into existing sources.
577
+
578
+ Args:
579
+ changes: Dict of section -> list of changed field names.
580
+ source_type: Source label ("env", "CLI", or "configure").
581
+
582
+ Returns:
583
+ New sources dict with changes merged in.
584
+ """
585
+ new_sources = self._copy_sources()
586
+
587
+ for section, field_names in changes.items():
588
+ section_sources = new_sources.setdefault(section, {})
589
+ for field_name in field_names:
590
+ if source_type == "env":
591
+ # Generate env var name based on convention
592
+ env_var = f"STKAI_{section.upper()}_{field_name.upper()}"
593
+ section_sources[field_name] = f"env:{env_var}"
594
+ else:
595
+ section_sources[field_name] = source_type
596
+
597
+ return new_sources
598
+
599
+ def _copy_sources(self) -> dict[str, dict[str, str]]:
600
+ """Create a deep copy of current sources."""
601
+ return {section: dict(flds) for section, flds in self.sources.items()}
602
+
603
+
333
604
  @dataclass(frozen=True)
334
605
  class STKAIConfig:
335
606
  """
336
607
  Global configuration for the stkai SDK.
337
608
 
338
- Aggregates all configuration sections: auth, rqc, agent, and rate_limit.
609
+ Aggregates all configuration sections: sdk, auth, rqc, agent, and rate_limit.
339
610
  Access via the global `STKAI.config` property.
340
611
 
341
612
  Attributes:
613
+ sdk: SDK metadata (version, cli_mode). Read-only.
342
614
  auth: Authentication configuration.
343
615
  rqc: RemoteQuickCommand configuration.
344
616
  agent: Agent configuration.
@@ -346,6 +618,10 @@ class STKAIConfig:
346
618
 
347
619
  Example:
348
620
  >>> from stkai import STKAI
621
+ >>> STKAI.config.sdk.version
622
+ '0.2.8'
623
+ >>> STKAI.config.sdk.cli_mode
624
+ True
349
625
  >>> STKAI.config.rqc.request_timeout
350
626
  30
351
627
  >>> STKAI.config.auth.has_credentials()
@@ -354,11 +630,14 @@ class STKAIConfig:
354
630
  False
355
631
  """
356
632
 
633
+ sdk: SdkConfig = field(default_factory=SdkConfig.detect)
357
634
  auth: AuthConfig = field(default_factory=AuthConfig)
358
635
  rqc: RqcConfig = field(default_factory=RqcConfig)
359
636
  agent: AgentConfig = field(default_factory=AgentConfig)
360
637
  rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
638
+ _tracker: STKAIConfigTracker = field(default_factory=STKAIConfigTracker, repr=False)
361
639
 
640
+ @STKAIConfigTracker.track_changes("env")
362
641
  def with_env_vars(self) -> STKAIConfig:
363
642
  """
364
643
  Return a new config with environment variables applied on top.
@@ -376,12 +655,121 @@ class STKAIConfig:
376
655
  >>> final = custom.with_env_vars()
377
656
  """
378
657
  return STKAIConfig(
658
+ sdk=self.sdk,
379
659
  auth=self.auth.with_overrides(_get_auth_from_env()),
380
660
  rqc=self.rqc.with_overrides(_get_rqc_from_env()),
381
661
  agent=self.agent.with_overrides(_get_agent_from_env()),
382
662
  rate_limit=self.rate_limit.with_overrides(_get_rate_limit_from_env()),
383
663
  )
384
664
 
665
+ @STKAIConfigTracker.track_changes("CLI")
666
+ def with_cli_defaults(self) -> STKAIConfig:
667
+ """
668
+ Return a new config with CLI-provided values applied.
669
+
670
+ CLI values take precedence over env vars. When running in CLI mode,
671
+ the CLI knows the correct endpoints for the current environment.
672
+
673
+ Returns:
674
+ New STKAIConfig instance with CLI values applied.
675
+
676
+ Example:
677
+ >>> # Apply CLI defaults on top of env vars
678
+ >>> config = STKAIConfig().with_env_vars().with_cli_defaults()
679
+ """
680
+ from stkai._cli import StkCLI
681
+
682
+ cli_rqc_overrides: dict[str, Any] = {}
683
+ if cli_base_url := StkCLI.get_codebuddy_base_url():
684
+ cli_rqc_overrides["base_url"] = cli_base_url
685
+
686
+ return STKAIConfig(
687
+ sdk=self.sdk,
688
+ auth=self.auth,
689
+ rqc=self.rqc.with_overrides(cli_rqc_overrides),
690
+ agent=self.agent,
691
+ rate_limit=self.rate_limit,
692
+ )
693
+
694
+ @STKAIConfigTracker.track_changes("user")
695
+ def with_section_overrides(
696
+ self,
697
+ *,
698
+ auth: dict[str, Any] | None = None,
699
+ rqc: dict[str, Any] | None = None,
700
+ agent: dict[str, Any] | None = None,
701
+ rate_limit: dict[str, Any] | None = None,
702
+ ) -> STKAIConfig:
703
+ """
704
+ Return a new config with overrides applied to nested sections.
705
+
706
+ Each section dict is merged with the existing section config,
707
+ only overriding the specified fields.
708
+
709
+ Args:
710
+ auth: Authentication config overrides.
711
+ rqc: RemoteQuickCommand config overrides.
712
+ agent: Agent config overrides.
713
+ rate_limit: Rate limiting config overrides.
714
+
715
+ Returns:
716
+ New STKAIConfig instance with overrides applied.
717
+
718
+ Example:
719
+ >>> config = STKAIConfig()
720
+ >>> custom = config.with_section_overrides(
721
+ ... rqc={"request_timeout": 60},
722
+ ... agent={"request_timeout": 120},
723
+ ... )
724
+ """
725
+ return STKAIConfig(
726
+ sdk=self.sdk,
727
+ auth=self.auth.with_overrides(auth or {}),
728
+ rqc=self.rqc.with_overrides(rqc or {}),
729
+ agent=self.agent.with_overrides(agent or {}),
730
+ rate_limit=self.rate_limit.with_overrides(rate_limit or {}),
731
+ )
732
+
733
+ def explain_data(self) -> dict[str, list[ConfigEntry]]:
734
+ """
735
+ Return config data structured for explain output.
736
+
737
+ Provides a structured representation of all config values and their
738
+ sources, useful for debugging, testing, or custom formatting.
739
+
740
+ Returns:
741
+ Dict mapping section names to list of ConfigEntry objects.
742
+
743
+ Example:
744
+ >>> config = STKAIConfig().with_env_vars()
745
+ >>> data = config.explain_data()
746
+ >>> for entry in data["rqc"]:
747
+ ... print(f"{entry.name}: {entry.value} ({entry.source})")
748
+ request_timeout: 30 (default)
749
+ ...
750
+ """
751
+ result: dict[str, list[ConfigEntry]] = {}
752
+
753
+ # SDK section (read-only, not tracked)
754
+ result["sdk"] = [
755
+ ConfigEntry(name=f.name, value=getattr(self.sdk, f.name), source="-")
756
+ for f in fields(self.sdk)
757
+ ]
758
+
759
+ for section_name in ("auth", "rqc", "agent", "rate_limit"):
760
+ section_config = getattr(self, section_name)
761
+ section_sources = self._tracker.sources.get(section_name, {})
762
+ result[section_name] = [
763
+ ConfigEntry(
764
+ name=f.name,
765
+ value=getattr(section_config, f.name),
766
+ source=section_sources.get(f.name, "default"),
767
+ )
768
+ for f in fields(section_config)
769
+ ]
770
+
771
+ return result
772
+
385
773
 
386
774
  # =============================================================================
387
775
  # Environment Variable Helpers
@@ -488,8 +876,8 @@ class _STKAI:
488
876
  """
489
877
 
490
878
  def __init__(self) -> None:
491
- """Initialize with defaults and apply environment variables."""
492
- self._config: STKAIConfig = STKAIConfig().with_env_vars()
879
+ """Initialize with defaults, environment variables, and CLI values."""
880
+ self._config: STKAIConfig = STKAIConfig().with_env_vars().with_cli_defaults()
493
881
 
494
882
  def configure(
495
883
  self,
@@ -499,6 +887,7 @@ class _STKAI:
499
887
  agent: dict[str, Any] | None = None,
500
888
  rate_limit: dict[str, Any] | None = None,
501
889
  allow_env_override: bool = True,
890
+ allow_cli_override: bool = True,
502
891
  ) -> STKAIConfig:
503
892
  """
504
893
  Configure SDK settings.
@@ -513,6 +902,8 @@ class _STKAI:
513
902
  rate_limit: Rate limiting config overrides (enabled, strategy, max_requests, etc.).
514
903
  allow_env_override: If True (default), env vars are used as fallback
515
904
  for fields NOT provided. If False, ignores env vars entirely.
905
+ allow_cli_override: If True (default), CLI values (oscli) are used as fallback
906
+ for fields NOT provided. If False, ignores CLI values entirely.
516
907
 
517
908
  Returns:
518
909
  The configured STKAIConfig instance.
@@ -520,11 +911,14 @@ class _STKAI:
520
911
  Raises:
521
912
  ValueError: If any dict contains unknown field names.
522
913
 
523
- Precedence (allow_env_override=True):
914
+ Precedence (both overrides True):
915
+ STKAI.configure() > CLI values > ENV vars > defaults
916
+
917
+ Precedence (allow_cli_override=False):
524
918
  STKAI.configure() > ENV vars > defaults
525
919
 
526
920
  Precedence (allow_env_override=False):
527
- STKAI.configure() > defaults
921
+ STKAI.configure() > CLI values > defaults
528
922
 
529
923
  Example:
530
924
  >>> from stkai import STKAI
@@ -534,17 +928,19 @@ class _STKAI:
534
928
  ... rate_limit={"enabled": True, "max_requests": 10},
535
929
  ... )
536
930
  """
537
- # Start with defaults, apply env vars as base layer (if enabled)
538
- base = STKAIConfig() # only defaults
931
+ # Start with defaults, apply env vars and CLI values as base layer
932
+ base = STKAIConfig()
539
933
  if allow_env_override:
540
934
  base = base.with_env_vars() # defaults + env vars
935
+ if allow_cli_override:
936
+ base = base.with_cli_defaults() # CLI values take precedence over env vars
541
937
 
542
938
  # Apply user overrides on top - configure() always wins
543
- self._config = STKAIConfig(
544
- auth=base.auth.with_overrides(auth or {}),
545
- rqc=base.rqc.with_overrides(rqc or {}),
546
- agent=base.agent.with_overrides(agent or {}),
547
- rate_limit=base.rate_limit.with_overrides(rate_limit or {}),
939
+ self._config = base.with_section_overrides(
940
+ auth=auth,
941
+ rqc=rqc,
942
+ agent=agent,
943
+ rate_limit=rate_limit,
548
944
  )
549
945
 
550
946
  return self._config
@@ -566,7 +962,7 @@ class _STKAI:
566
962
 
567
963
  def reset(self) -> STKAIConfig:
568
964
  """
569
- Reset configuration to defaults + env vars.
965
+ Reset configuration to defaults + env vars + CLI values.
570
966
 
571
967
  Useful for testing to ensure clean state between tests.
572
968
 
@@ -577,9 +973,63 @@ class _STKAI:
577
973
  >>> from stkai import STKAI
578
974
  >>> STKAI.reset()
579
975
  """
580
- self._config = STKAIConfig().with_env_vars()
976
+ self._config = STKAIConfig().with_env_vars().with_cli_defaults()
581
977
  return self._config
582
978
 
979
+ def explain(
980
+ self,
981
+ output: Callable[[str], None] = print,
982
+ ) -> None:
983
+ """
984
+ Print current configuration with sources.
985
+
986
+ Useful for debugging and troubleshooting configuration issues.
987
+ Shows each config value and where it came from:
988
+
989
+ - "default": Using hardcoded default value
990
+ - "env:VAR_NAME": Value from environment variable
991
+ - "CLI": Value from StackSpot CLI (oscli)
992
+ - "configure": Value set via STKAI.configure()
993
+
994
+ Args:
995
+ output: Callable to output each line. Defaults to print.
996
+ Can be used with logging: `STKAI.explain(logger.info)`
997
+
998
+ Example:
999
+ >>> from stkai import STKAI
1000
+ >>> STKAI.explain()
1001
+ STKAI Configuration:
1002
+ ====================
1003
+ [rqc]
1004
+ base_url .......... https://example.com (CLI)
1005
+ request_timeout ... 60 (configure)
1006
+ ...
1007
+
1008
+ >>> # Using with logging
1009
+ >>> import logging
1010
+ >>> STKAI.explain(logging.info)
1011
+ """
1012
+ name_width = 25 # field name + dots
1013
+ value_width = 50 # max value width (matches truncation)
1014
+ total_width = 2 + name_width + 2 + (value_width + 2) + 1 + 8 # matches separator
1015
+
1016
+ output("STKAI Configuration:")
1017
+ output("=" * total_width)
1018
+
1019
+ # Header
1020
+ output(f" {'Field':<{name_width}} │ {'Value':<{value_width}} │ Source")
1021
+ output(f"--{'-' * name_width}-+{'-' * (value_width + 2)}+--------")
1022
+
1023
+ for section_name, entries in self._config.explain_data().items():
1024
+ output(f"[{section_name}]")
1025
+ for entry in entries:
1026
+ dots = "." * (name_width - len(entry.name))
1027
+ value_padded = entry.formatted_value.ljust(value_width)
1028
+ marker = "✎" if entry.source not in ("default", "-") else " "
1029
+ output(f" {entry.name} {dots} {value_padded} {marker} {entry.source}")
1030
+
1031
+ output("=" * total_width)
1032
+
583
1033
  def __repr__(self) -> str:
584
1034
  return f"STKAI(config={self._config!r})"
585
1035