oasr 0.3.4__py3-none-any.whl → 0.4.1__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.
commands/config.py ADDED
@@ -0,0 +1,163 @@
1
+ """Config command - manage OASR configuration."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from agents import detect_available_agents, get_all_agent_names
7
+ from config import CONFIG_FILE, load_config, save_config
8
+
9
+
10
+ def register(subparsers: argparse._SubParsersAction) -> None:
11
+ """Register the config command."""
12
+ parser = subparsers.add_parser(
13
+ "config",
14
+ help="Manage configuration",
15
+ description="Manage OASR configuration settings",
16
+ )
17
+
18
+ config_subparsers = parser.add_subparsers(dest="config_action", help="Config actions")
19
+
20
+ # config set
21
+ set_parser = config_subparsers.add_parser(
22
+ "set",
23
+ help="Set a configuration value",
24
+ description="Set a configuration value",
25
+ )
26
+ set_parser.add_argument("key", help="Configuration key (e.g., 'agent')")
27
+ set_parser.add_argument("value", help="Configuration value")
28
+ set_parser.set_defaults(func=run_set)
29
+
30
+ # config get
31
+ get_parser = config_subparsers.add_parser(
32
+ "get",
33
+ help="Get a configuration value",
34
+ description="Get a configuration value",
35
+ )
36
+ get_parser.add_argument("key", help="Configuration key (e.g., 'agent')")
37
+ get_parser.set_defaults(func=run_get)
38
+
39
+ # config list
40
+ list_parser = config_subparsers.add_parser(
41
+ "list",
42
+ help="List all configuration",
43
+ description="List all configuration settings",
44
+ )
45
+ list_parser.set_defaults(func=run_list)
46
+
47
+ # config path
48
+ path_parser = config_subparsers.add_parser(
49
+ "path",
50
+ help="Show config file path",
51
+ description="Show configuration file path",
52
+ )
53
+ path_parser.set_defaults(func=run_path)
54
+
55
+ # Default to showing help if no subcommand
56
+ parser.set_defaults(func=lambda args: parser.print_help() or 1)
57
+
58
+
59
+ def run_set(args: argparse.Namespace) -> int:
60
+ """Set a configuration value."""
61
+ key = args.key.lower()
62
+ value = args.value
63
+
64
+ # Only support agent for now
65
+ if key == "agent":
66
+ # Validate agent name
67
+ valid_agents = get_all_agent_names()
68
+ if value not in valid_agents:
69
+ print(
70
+ f"Error: Invalid agent '{value}'. Must be one of: {', '.join(valid_agents)}",
71
+ file=sys.stderr,
72
+ )
73
+ return 1
74
+
75
+ # Load config, update, save
76
+ config = load_config(args.config if hasattr(args, "config") else None)
77
+ config["agent"]["default"] = value
78
+ save_config(config, args.config if hasattr(args, "config") else None)
79
+
80
+ # Show available vs configured
81
+ available = detect_available_agents()
82
+ if value in available:
83
+ print(f"✓ Default agent set to: {value}")
84
+ else:
85
+ print(f"✓ Default agent set to: {value}")
86
+ print(
87
+ f" Warning: '{value}' binary not found in PATH. Install it to use this agent.",
88
+ file=sys.stderr,
89
+ )
90
+
91
+ return 0
92
+ else:
93
+ print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
94
+ return 1
95
+
96
+
97
+ def run_get(args: argparse.Namespace) -> int:
98
+ """Get a configuration value."""
99
+ key = args.key.lower()
100
+
101
+ config = load_config(args.config if hasattr(args, "config") else None)
102
+
103
+ if key == "agent":
104
+ agent = config["agent"].get("default")
105
+ if agent:
106
+ print(agent)
107
+ else:
108
+ print("No default agent configured", file=sys.stderr)
109
+ return 1
110
+ return 0
111
+ else:
112
+ print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
113
+ return 1
114
+
115
+
116
+ def run_list(args: argparse.Namespace) -> int:
117
+ """List all configuration."""
118
+ config = load_config(args.config if hasattr(args, "config") else None)
119
+
120
+ print("Configuration:")
121
+ print()
122
+
123
+ # Agent section
124
+ print(" [agent]")
125
+ agent = config["agent"].get("default")
126
+ if agent:
127
+ available = detect_available_agents()
128
+ status = "✓" if agent in available else "✗"
129
+ print(f" default = {agent} {status}")
130
+ else:
131
+ print(" default = (not set)")
132
+ print()
133
+
134
+ # Show available agents
135
+ available = detect_available_agents()
136
+ if available:
137
+ print(f" Available agents: {', '.join(available)}")
138
+ else:
139
+ print(" Available agents: (none detected)")
140
+ print()
141
+
142
+ # Validation section
143
+ print(" [validation]")
144
+ print(f" reference_max_lines = {config['validation']['reference_max_lines']}")
145
+ print(f" strict = {config['validation']['strict']}")
146
+ print()
147
+
148
+ # Adapter section
149
+ print(" [adapter]")
150
+ print(f" default_targets = {config['adapter']['default_targets']}")
151
+ print()
152
+
153
+ return 0
154
+
155
+
156
+ def run_path(args: argparse.Namespace) -> int:
157
+ """Show config file path."""
158
+ if hasattr(args, "config") and args.config:
159
+ config_path = args.config
160
+ else:
161
+ config_path = CONFIG_FILE
162
+ print(config_path)
163
+ return 0
commands/exec.py ADDED
@@ -0,0 +1,199 @@
1
+ """Execute a skill from the registry using an agent CLI."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from agents.registry import detect_available_agents, get_driver
8
+ from config import load_config
9
+ from registry import load_registry
10
+
11
+
12
+ def register(subparsers):
13
+ """Register the exec command."""
14
+ setup_parser(subparsers)
15
+
16
+
17
+ def setup_parser(subparsers):
18
+ """Set up the exec command parser."""
19
+ parser = subparsers.add_parser(
20
+ "exec",
21
+ help="Execute a skill from the registry",
22
+ description="Execute a skill from the registry using an agent CLI. "
23
+ "The skill is executed in the current working directory.",
24
+ )
25
+ parser.add_argument(
26
+ "skill_name",
27
+ help="Name of the skill to execute from the registry",
28
+ )
29
+ parser.add_argument(
30
+ "-p",
31
+ "--prompt",
32
+ help="Inline prompt/instructions for the agent",
33
+ )
34
+ parser.add_argument(
35
+ "-i",
36
+ "--instructions",
37
+ metavar="FILE",
38
+ help="Read prompt/instructions from a file",
39
+ )
40
+ parser.add_argument(
41
+ "-a",
42
+ "--agent",
43
+ help="Override the default agent (codex, copilot, claude, opencode)",
44
+ )
45
+ parser.set_defaults(func=run)
46
+
47
+
48
+ def run(args: argparse.Namespace) -> int:
49
+ """Execute a skill from the registry."""
50
+ # Load registry to find the skill
51
+ entries = load_registry()
52
+ entry_map = {e.name: e for e in entries}
53
+ skill_name = args.skill_name
54
+
55
+ if skill_name not in entry_map:
56
+ print(f"Error: Skill '{skill_name}' not found in registry", file=sys.stderr)
57
+ print("\nUse 'oasr registry list' to see available skills.", file=sys.stderr)
58
+ return 1
59
+
60
+ skill_entry = entry_map[skill_name]
61
+ skill_source = skill_entry.path
62
+
63
+ if not skill_source:
64
+ print(f"Error: Skill '{skill_name}' has no source configured", file=sys.stderr)
65
+ return 1
66
+
67
+ # Get the skill content - look for SKILL.md in the skill directory
68
+ skill_dir = Path(skill_source)
69
+ skill_path = skill_dir / "SKILL.md"
70
+
71
+ if not skill_path.exists():
72
+ print(f"Error: Skill file not found: {skill_path}", file=sys.stderr)
73
+ print("\nTry running 'oasr sync' to update your skills.", file=sys.stderr)
74
+ return 1
75
+
76
+ try:
77
+ skill_content = skill_path.read_text(encoding="utf-8")
78
+ except Exception as e:
79
+ print(f"Error reading skill file: {e}", file=sys.stderr)
80
+ return 1
81
+
82
+ # Get the user prompt from various sources
83
+ user_prompt = _get_user_prompt(args)
84
+ if user_prompt is None:
85
+ # Error already printed by _get_user_prompt
86
+ return 1
87
+
88
+ # Determine which agent to use
89
+ agent_name = _get_agent_name(args)
90
+ if agent_name is None:
91
+ # Error already printed by _get_agent_name
92
+ return 1
93
+
94
+ # Get the agent driver
95
+ try:
96
+ driver = get_driver(agent_name)
97
+ except ValueError as e:
98
+ print(f"Error: {e}", file=sys.stderr)
99
+ return 1
100
+
101
+ # Execute the skill
102
+ print(f"Executing skill '{skill_name}' with {agent_name}...", file=sys.stderr)
103
+ print("━" * 60, file=sys.stderr)
104
+
105
+ try:
106
+ result = driver.execute(skill_content, user_prompt)
107
+ # CompletedProcess has returncode attribute (0 = success)
108
+ # Output was already streamed to stdout since capture_output=False
109
+ return result.returncode
110
+ except Exception as e:
111
+ print(f"\nUnexpected error: {e}", file=sys.stderr)
112
+ return 1
113
+
114
+
115
+ def _get_user_prompt(args: argparse.Namespace) -> str | None:
116
+ """Get the user prompt from CLI args or stdin.
117
+
118
+ Returns None if there's an error, with error message printed to stderr.
119
+ """
120
+ # Check for conflicting options
121
+ if args.prompt and args.instructions:
122
+ print(
123
+ "Error: Cannot use both --prompt and --instructions at the same time",
124
+ file=sys.stderr,
125
+ )
126
+ return None
127
+
128
+ # Option 1: Inline prompt via -p/--prompt
129
+ if args.prompt:
130
+ return args.prompt
131
+
132
+ # Option 2: File-based instructions via -i/--instructions
133
+ if args.instructions:
134
+ instructions_path = Path(args.instructions)
135
+ if not instructions_path.exists():
136
+ print(f"Error: Instructions file not found: {args.instructions}", file=sys.stderr)
137
+ return None
138
+
139
+ try:
140
+ return instructions_path.read_text(encoding="utf-8")
141
+ except Exception as e:
142
+ print(f"Error reading instructions file: {e}", file=sys.stderr)
143
+ return None
144
+
145
+ # Option 3: Read from stdin
146
+ if not sys.stdin.isatty():
147
+ try:
148
+ return sys.stdin.read()
149
+ except Exception as e:
150
+ print(f"Error reading from stdin: {e}", file=sys.stderr)
151
+ return None
152
+
153
+ # No prompt provided
154
+ print("Error: No prompt provided", file=sys.stderr)
155
+ print("\nProvide a prompt using one of:", file=sys.stderr)
156
+ print(" -p/--prompt 'Your prompt here'", file=sys.stderr)
157
+ print(" -i/--instructions path/to/file.txt", file=sys.stderr)
158
+ print(" echo 'Your prompt' | oasr exec <skill>", file=sys.stderr)
159
+ return None
160
+
161
+
162
+ def _get_agent_name(args: argparse.Namespace) -> str | None:
163
+ """Get the agent name from CLI flag or config.
164
+
165
+ Returns None if there's an error, with error message printed to stderr.
166
+ """
167
+ # Option 1: Explicit --agent flag
168
+ if args.agent:
169
+ agent_name = args.agent.lower()
170
+ # Validate it's a known and available agent
171
+ available = detect_available_agents()
172
+ if agent_name not in available or not available[agent_name]:
173
+ print(f"Error: Agent '{agent_name}' is not available", file=sys.stderr)
174
+ print("\nAvailable agents:", file=sys.stderr)
175
+ for name in sorted(available.keys()):
176
+ status = "✓" if available[name] else "✗"
177
+ print(f" {status} {name}", file=sys.stderr)
178
+ return None
179
+ return agent_name
180
+
181
+ # Option 2: Default from config
182
+ config = load_config()
183
+ default_agent = config.get("agent", {}).get("default")
184
+
185
+ if default_agent:
186
+ return default_agent
187
+
188
+ # No agent configured
189
+ print("Error: No agent configured", file=sys.stderr)
190
+ print("\nConfigure a default agent with:", file=sys.stderr)
191
+ print(" oasr config set agent <agent-name>", file=sys.stderr)
192
+ print("\nOr specify an agent for this command:", file=sys.stderr)
193
+ print(" oasr exec --agent <agent-name> <skill>", file=sys.stderr)
194
+ print("\nAvailable agents:", file=sys.stderr)
195
+ available = detect_available_agents()
196
+ for name in sorted(available.keys()):
197
+ status = "✓" if available[name] else "✗"
198
+ print(f" {status} {name}", file=sys.stderr)
199
+ return None
commands/use.py CHANGED
@@ -1,19 +1,21 @@
1
- """`asr use` command."""
1
+ """`asr use` command - DEPRECATED, use `oasr clone` instead."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
- import fnmatch
7
- import json
8
6
  import sys
9
7
  from pathlib import Path
10
8
 
11
- from registry import load_registry
12
- from skillcopy import copy_skill
9
+ from commands import clone
13
10
 
14
11
 
15
12
  def register(subparsers) -> None:
16
- p = subparsers.add_parser("use", help="Copy skill(s) to target directory")
13
+ """Register the deprecated use command."""
14
+ p = subparsers.add_parser(
15
+ "use",
16
+ help="[DEPRECATED] Copy skill(s) - use 'clone' instead",
17
+ description="DEPRECATED: Use 'oasr clone' instead. This command will be removed in v0.5.0.",
18
+ )
17
19
  p.add_argument("names", nargs="+", help="Skill name(s) or glob pattern(s) to copy")
18
20
  p.add_argument(
19
21
  "-d",
@@ -28,145 +30,16 @@ def register(subparsers) -> None:
28
30
  p.set_defaults(func=run)
29
31
 
30
32
 
31
- def _match_skills(patterns: list[str], entry_map: dict) -> tuple[list[str], list[str]]:
32
- """Match skill names against patterns (exact or glob).
33
-
34
- Returns:
35
- Tuple of (matched_names, unmatched_patterns).
36
- """
37
- matched = set()
38
- unmatched = []
39
- all_names = list(entry_map.keys())
40
-
41
- for pattern in patterns:
42
- if pattern in entry_map:
43
- matched.add(pattern)
44
- elif any(c in pattern for c in "*?["):
45
- # Glob pattern
46
- matches = fnmatch.filter(all_names, pattern)
47
- if matches:
48
- matched.update(matches)
49
- else:
50
- unmatched.append(pattern)
51
- else:
52
- unmatched.append(pattern)
53
-
54
- return list(matched), unmatched
55
-
56
-
57
33
  def run(args: argparse.Namespace) -> int:
58
- entries = load_registry()
59
- entry_map = {e.name: e for e in entries}
60
-
61
- output_dir = args.output_dir.resolve()
62
- output_dir.mkdir(parents=True, exist_ok=True)
63
-
64
- copied = []
65
- warnings = []
66
-
67
- matched_names, unmatched = _match_skills(args.names, entry_map)
68
-
69
- for pattern in unmatched:
70
- warnings.append(f"No skills matched: {pattern}")
71
-
72
- # Get manifests for tracking metadata
73
- from manifest import load_manifest
74
-
75
- # Separate remote and local skills for parallel processing
76
- from skillcopy.remote import is_remote_source
77
-
78
- remote_names = [name for name in matched_names if is_remote_source(entry_map[name].path)]
79
- local_names = [name for name in matched_names if not is_remote_source(entry_map[name].path)]
80
-
81
- # Handle remote skills with parallel fetching
82
- if remote_names:
83
- print(f"Fetching {len(remote_names)} remote skill(s)...", file=sys.stderr)
84
- import threading
85
- from concurrent.futures import ThreadPoolExecutor, as_completed
86
-
87
- print_lock = threading.Lock()
88
-
89
- def copy_remote_entry(name):
90
- """Copy a remote skill with thread-safe progress."""
91
- entry = entry_map[name]
92
- dest = output_dir / name
93
-
94
- try:
95
- with print_lock:
96
- platform = (
97
- "GitHub" if "github.com" in entry.path else "GitLab" if "gitlab.com" in entry.path else "remote"
98
- )
99
- print(f" ↓ {name} (fetching from {platform}...)", file=sys.stderr, flush=True)
100
-
101
- # Get manifest hash for tracking
102
- manifest = load_manifest(name)
103
- source_hash = manifest.content_hash if manifest else None
104
-
105
- copy_skill(
106
- entry.path,
107
- dest,
108
- validate=False,
109
- show_progress=False,
110
- skill_name=name,
111
- inject_tracking=True,
112
- source_hash=source_hash,
113
- )
114
-
115
- with print_lock:
116
- print(f" ✓ {name} (downloaded)", file=sys.stderr)
117
-
118
- return {"name": name, "src": entry.path, "dest": str(dest)}, None
119
- except Exception as e:
120
- with print_lock:
121
- print(f" ✗ {name} ({str(e)[:50]}...)", file=sys.stderr)
122
- return None, f"Failed to copy {name}: {e}"
123
-
124
- # Copy remote skills in parallel
125
- with ThreadPoolExecutor(max_workers=4) as executor:
126
- futures = {executor.submit(copy_remote_entry, name): name for name in remote_names}
127
-
128
- for future in as_completed(futures):
129
- result, error = future.result()
130
- if result:
131
- copied.append(result)
132
- if error:
133
- warnings.append(error)
134
-
135
- # Handle local skills sequentially (fast anyway)
136
- for name in sorted(local_names):
137
- entry = entry_map[name]
138
- dest = output_dir / name
139
-
140
- try:
141
- # Get manifest hash for tracking
142
- manifest = load_manifest(name)
143
- source_hash = manifest.content_hash if manifest else None
144
-
145
- # Unified copy with tracking
146
- copy_skill(entry.path, dest, validate=False, inject_tracking=True, source_hash=source_hash)
147
- copied.append({"name": name, "src": entry.path, "dest": str(dest)})
148
- except Exception as e:
149
- warnings.append(f"Failed to copy {name}: {e}")
150
-
151
- if not args.quiet:
152
- for w in warnings:
153
- print(f"⚠ {w}", file=sys.stderr)
154
-
155
- if args.json:
34
+ """Execute the deprecated use command (delegates to clone)."""
35
+ # Show deprecation warning unless --quiet or --json
36
+ if not args.quiet and not args.json:
156
37
  print(
157
- json.dumps(
158
- {
159
- "copied": len(copied),
160
- "warnings": len(warnings),
161
- "skills": copied,
162
- },
163
- indent=2,
164
- )
38
+ "⚠ Warning: 'oasr use' is deprecated. Use 'oasr clone' instead.",
39
+ file=sys.stderr,
165
40
  )
166
- else:
167
- for c in copied:
168
- print(f"Copied: {c['name']} → {c['dest']}")
169
- if copied:
170
- print(f"\n{len(copied)} skill(s) copied to {output_dir}")
41
+ print(" This command will be removed in v0.5.0.", file=sys.stderr)
42
+ print(file=sys.stderr)
171
43
 
172
- return 1 if warnings and not copied else 0
44
+ # Delegate to clone command
45
+ return clone.run(args)
@@ -11,18 +11,21 @@ else:
11
11
 
12
12
  import tomli_w
13
13
 
14
+ from config.defaults import DEFAULT_CONFIG
15
+ from config.schema import validate_config
16
+
14
17
  OASR_DIR = Path.home() / ".oasr"
15
18
  CONFIG_FILE = OASR_DIR / "config.toml"
16
19
 
17
- DEFAULT_CONFIG: dict[str, Any] = {
18
- "validation": {
19
- "reference_max_lines": 500,
20
- "strict": False,
21
- },
22
- "adapter": {
23
- "default_targets": ["cursor", "windsurf"],
24
- },
25
- }
20
+ __all__ = [
21
+ "OASR_DIR",
22
+ "CONFIG_FILE",
23
+ "ensure_oasr_dir",
24
+ "ensure_skills_dir",
25
+ "load_config",
26
+ "save_config",
27
+ "get_default_config",
28
+ ]
26
29
 
27
30
 
28
31
  def ensure_oasr_dir() -> Path:
@@ -48,9 +51,12 @@ def load_config(config_path: Path | None = None) -> dict[str, Any]:
48
51
  """
49
52
  path = config_path or CONFIG_FILE
50
53
 
51
- config = DEFAULT_CONFIG.copy()
52
- config["validation"] = DEFAULT_CONFIG["validation"].copy()
53
- config["adapter"] = DEFAULT_CONFIG["adapter"].copy()
54
+ # Deep copy defaults
55
+ config = {
56
+ "validation": DEFAULT_CONFIG["validation"].copy(),
57
+ "adapter": DEFAULT_CONFIG["adapter"].copy(),
58
+ "agent": DEFAULT_CONFIG["agent"].copy(),
59
+ }
54
60
 
55
61
  if path.exists():
56
62
  with open(path, "rb") as f:
@@ -60,6 +66,8 @@ def load_config(config_path: Path | None = None) -> dict[str, Any]:
60
66
  config["validation"].update(loaded["validation"])
61
67
  if "adapter" in loaded:
62
68
  config["adapter"].update(loaded["adapter"])
69
+ if "agent" in loaded:
70
+ config["agent"].update(loaded["agent"])
63
71
 
64
72
  return config
65
73
 
@@ -70,17 +78,32 @@ def save_config(config: dict[str, Any], config_path: Path | None = None) -> None
70
78
  Args:
71
79
  config: Configuration dictionary to save.
72
80
  config_path: Override config file path. Defaults to ~/.oasr/config.toml.
81
+
82
+ Raises:
83
+ ValueError: If configuration is invalid.
73
84
  """
85
+ validate_config(config)
86
+
74
87
  path = config_path or CONFIG_FILE
75
88
  ensure_oasr_dir()
76
89
 
90
+ # Deep copy and remove None values (TOML can't serialize None)
91
+ config_to_save = {}
92
+ for section, values in config.items():
93
+ if isinstance(values, dict):
94
+ config_to_save[section] = {k: v for k, v in values.items() if v is not None}
95
+ else:
96
+ if values is not None:
97
+ config_to_save[section] = values
98
+
77
99
  with open(path, "wb") as f:
78
- tomli_w.dump(config, f)
100
+ tomli_w.dump(config_to_save, f)
79
101
 
80
102
 
81
103
  def get_default_config() -> dict[str, Any]:
82
104
  """Return a copy of the default configuration."""
83
- config = DEFAULT_CONFIG.copy()
84
- config["validation"] = DEFAULT_CONFIG["validation"].copy()
85
- config["adapter"] = DEFAULT_CONFIG["adapter"].copy()
86
- return config
105
+ return {
106
+ "validation": DEFAULT_CONFIG["validation"].copy(),
107
+ "adapter": DEFAULT_CONFIG["adapter"].copy(),
108
+ "agent": DEFAULT_CONFIG["agent"].copy(),
109
+ }
config/defaults.py ADDED
@@ -0,0 +1,16 @@
1
+ """Default configuration values."""
2
+
3
+ from typing import Any
4
+
5
+ DEFAULT_CONFIG: dict[str, Any] = {
6
+ "validation": {
7
+ "reference_max_lines": 500,
8
+ "strict": False,
9
+ },
10
+ "adapter": {
11
+ "default_targets": ["cursor", "windsurf"],
12
+ },
13
+ "agent": {
14
+ "default": None,
15
+ },
16
+ }
config/schema.py ADDED
@@ -0,0 +1,36 @@
1
+ """Configuration schema validation."""
2
+
3
+ from typing import Any
4
+
5
+ VALID_AGENTS = {"codex", "copilot", "claude", "opencode"}
6
+
7
+
8
+ def validate_config(config: dict[str, Any]) -> None:
9
+ """Validate configuration dictionary.
10
+
11
+ Args:
12
+ config: Configuration dictionary to validate.
13
+
14
+ Raises:
15
+ ValueError: If configuration is invalid.
16
+ """
17
+ if "agent" in config and "default" in config["agent"]:
18
+ agent = config["agent"]["default"]
19
+ if agent is not None and agent not in VALID_AGENTS:
20
+ raise ValueError(f"Invalid agent '{agent}'. Must be one of: {', '.join(sorted(VALID_AGENTS))}")
21
+
22
+ if "validation" in config:
23
+ if "reference_max_lines" in config["validation"]:
24
+ max_lines = config["validation"]["reference_max_lines"]
25
+ if not isinstance(max_lines, int) or max_lines < 1:
26
+ raise ValueError("validation.reference_max_lines must be a positive integer")
27
+
28
+ if "strict" in config["validation"]:
29
+ if not isinstance(config["validation"]["strict"], bool):
30
+ raise ValueError("validation.strict must be a boolean")
31
+
32
+ if "adapter" in config:
33
+ if "default_targets" in config["adapter"]:
34
+ targets = config["adapter"]["default_targets"]
35
+ if not isinstance(targets, list):
36
+ raise ValueError("adapter.default_targets must be a list")