oasr 0.5.2__py3-none-any.whl → 0.6.0__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.
- agents/base.py +17 -3
- agents/claude.py +12 -2
- agents/codex.py +12 -2
- agents/copilot.py +12 -2
- agents/opencode.py +12 -2
- cli.py +3 -2
- commands/config.py +284 -37
- commands/exec.py +21 -1
- commands/profile.py +84 -0
- commands/update.py +89 -7
- completions/bash.sh +19 -6
- completions/fish.fish +21 -7
- completions/powershell.ps1 +77 -8
- completions/zsh.sh +31 -5
- config/__init__.py +11 -0
- config/defaults.py +4 -21
- config/schema.py +3 -29
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/METADATA +31 -21
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/RECORD +32 -24
- policy/defaults.py +3 -19
- policy/profile.py +5 -7
- profiles/__init__.py +23 -0
- profiles/builtins.py +63 -0
- profiles/loader.py +74 -0
- profiles/paths.py +22 -0
- profiles/registry.py +19 -0
- profiles/summary.py +23 -0
- profiles/validation.py +34 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/WHEEL +0 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/entry_points.txt +0 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {oasr-0.5.2.dist-info → oasr-0.6.0.dist-info}/licenses/NOTICE +0 -0
agents/base.py
CHANGED
|
@@ -34,25 +34,39 @@ class AgentDriver(ABC):
|
|
|
34
34
|
return shutil.which(self.get_binary_name()) is not None
|
|
35
35
|
|
|
36
36
|
@abstractmethod
|
|
37
|
-
def build_command(
|
|
37
|
+
def build_command(
|
|
38
|
+
self,
|
|
39
|
+
skill_content: str,
|
|
40
|
+
user_prompt: str,
|
|
41
|
+
cwd: Path,
|
|
42
|
+
extra_args: list[str] | None = None,
|
|
43
|
+
) -> list[str]:
|
|
38
44
|
"""Build the command to execute.
|
|
39
45
|
|
|
40
46
|
Args:
|
|
41
47
|
skill_content: Full SKILL.md content.
|
|
42
48
|
user_prompt: User's prompt/request.
|
|
43
49
|
cwd: Current working directory.
|
|
50
|
+
extra_args: Additional CLI arguments for the agent (optional).
|
|
44
51
|
|
|
45
52
|
Returns:
|
|
46
53
|
Command as list of strings (for subprocess).
|
|
47
54
|
"""
|
|
48
55
|
|
|
49
|
-
def execute(
|
|
56
|
+
def execute(
|
|
57
|
+
self,
|
|
58
|
+
skill_content: str,
|
|
59
|
+
user_prompt: str,
|
|
60
|
+
cwd: Path | None = None,
|
|
61
|
+
extra_args: list[str] | None = None,
|
|
62
|
+
) -> subprocess.CompletedProcess:
|
|
50
63
|
"""Execute skill with agent.
|
|
51
64
|
|
|
52
65
|
Args:
|
|
53
66
|
skill_content: Full SKILL.md content.
|
|
54
67
|
user_prompt: User's prompt/request.
|
|
55
68
|
cwd: Working directory for execution (defaults to current dir).
|
|
69
|
+
extra_args: Additional CLI arguments for the agent (optional).
|
|
56
70
|
|
|
57
71
|
Returns:
|
|
58
72
|
CompletedProcess with stdout/stderr/returncode.
|
|
@@ -64,7 +78,7 @@ class AgentDriver(ABC):
|
|
|
64
78
|
raise FileNotFoundError(f"{self.get_name()} binary '{self.get_binary_name()}' not found in PATH")
|
|
65
79
|
|
|
66
80
|
working_dir = cwd or Path.cwd()
|
|
67
|
-
cmd = self.build_command(skill_content, user_prompt, working_dir)
|
|
81
|
+
cmd = self.build_command(skill_content, user_prompt, working_dir, extra_args=extra_args)
|
|
68
82
|
|
|
69
83
|
return subprocess.run(
|
|
70
84
|
cmd,
|
agents/claude.py
CHANGED
|
@@ -16,10 +16,20 @@ class ClaudeDriver(AgentDriver):
|
|
|
16
16
|
"""Get the CLI binary name."""
|
|
17
17
|
return "claude"
|
|
18
18
|
|
|
19
|
-
def build_command(
|
|
19
|
+
def build_command(
|
|
20
|
+
self,
|
|
21
|
+
skill_content: str,
|
|
22
|
+
user_prompt: str,
|
|
23
|
+
cwd: Path,
|
|
24
|
+
extra_args: list[str] | None = None,
|
|
25
|
+
) -> list[str]:
|
|
20
26
|
"""Build claude command.
|
|
21
27
|
|
|
22
28
|
Claude syntax: claude <prompt> -p
|
|
23
29
|
"""
|
|
24
30
|
injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
|
|
25
|
-
|
|
31
|
+
cmd = ["claude"]
|
|
32
|
+
if extra_args:
|
|
33
|
+
cmd.extend(extra_args)
|
|
34
|
+
cmd.extend([injected_prompt, "-p"])
|
|
35
|
+
return cmd
|
agents/codex.py
CHANGED
|
@@ -16,10 +16,20 @@ class CodexDriver(AgentDriver):
|
|
|
16
16
|
"""Get the CLI binary name."""
|
|
17
17
|
return "codex"
|
|
18
18
|
|
|
19
|
-
def build_command(
|
|
19
|
+
def build_command(
|
|
20
|
+
self,
|
|
21
|
+
skill_content: str,
|
|
22
|
+
user_prompt: str,
|
|
23
|
+
cwd: Path,
|
|
24
|
+
extra_args: list[str] | None = None,
|
|
25
|
+
) -> list[str]:
|
|
20
26
|
"""Build codex exec command.
|
|
21
27
|
|
|
22
28
|
Codex syntax: codex exec "<prompt>"
|
|
23
29
|
"""
|
|
24
30
|
injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
|
|
25
|
-
|
|
31
|
+
cmd = ["codex", "exec"]
|
|
32
|
+
if extra_args:
|
|
33
|
+
cmd.extend(extra_args)
|
|
34
|
+
cmd.append(injected_prompt)
|
|
35
|
+
return cmd
|
agents/copilot.py
CHANGED
|
@@ -16,10 +16,20 @@ class CopilotDriver(AgentDriver):
|
|
|
16
16
|
"""Get the CLI binary name."""
|
|
17
17
|
return "copilot"
|
|
18
18
|
|
|
19
|
-
def build_command(
|
|
19
|
+
def build_command(
|
|
20
|
+
self,
|
|
21
|
+
skill_content: str,
|
|
22
|
+
user_prompt: str,
|
|
23
|
+
cwd: Path,
|
|
24
|
+
extra_args: list[str] | None = None,
|
|
25
|
+
) -> list[str]:
|
|
20
26
|
"""Build copilot command.
|
|
21
27
|
|
|
22
28
|
Copilot syntax: copilot -p "<prompt>"
|
|
23
29
|
"""
|
|
24
30
|
injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
|
|
25
|
-
|
|
31
|
+
cmd = ["copilot"]
|
|
32
|
+
if extra_args:
|
|
33
|
+
cmd.extend(extra_args)
|
|
34
|
+
cmd.extend(["-p", injected_prompt])
|
|
35
|
+
return cmd
|
agents/opencode.py
CHANGED
|
@@ -16,10 +16,20 @@ class OpenCodeDriver(AgentDriver):
|
|
|
16
16
|
"""Get the CLI binary name."""
|
|
17
17
|
return "opencode"
|
|
18
18
|
|
|
19
|
-
def build_command(
|
|
19
|
+
def build_command(
|
|
20
|
+
self,
|
|
21
|
+
skill_content: str,
|
|
22
|
+
user_prompt: str,
|
|
23
|
+
cwd: Path,
|
|
24
|
+
extra_args: list[str] | None = None,
|
|
25
|
+
) -> list[str]:
|
|
20
26
|
"""Build opencode run command.
|
|
21
27
|
|
|
22
28
|
OpenCode syntax: opencode run "<prompt>"
|
|
23
29
|
"""
|
|
24
30
|
injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
|
|
25
|
-
|
|
31
|
+
cmd = ["opencode", "run"]
|
|
32
|
+
if extra_args:
|
|
33
|
+
cmd.extend(extra_args)
|
|
34
|
+
cmd.append(injected_prompt)
|
|
35
|
+
return cmd
|
cli.py
CHANGED
|
@@ -10,10 +10,10 @@ import json
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
from commands import adapter, clean, clone, config, diff, exec, find, registry, sync, update, use, validate
|
|
13
|
+
from commands import adapter, clean, clone, config, diff, exec, find, profile, registry, sync, update, use, validate
|
|
14
14
|
from commands import help as help_cmd
|
|
15
15
|
|
|
16
|
-
__version__ = "0.
|
|
16
|
+
__version__ = "0.6.0"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -72,6 +72,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
72
72
|
diff.register(subparsers) # Show tracked skill status
|
|
73
73
|
sync.register(subparsers) # Refresh tracked skills
|
|
74
74
|
config.register(subparsers) # Configuration management
|
|
75
|
+
profile.register(subparsers) # Profile selection
|
|
75
76
|
clone.register(subparsers) # Clone skills to directory
|
|
76
77
|
exec.register(subparsers) # Execute skills with agent CLI
|
|
77
78
|
|
commands/config.py
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 11):
|
|
9
|
+
import tomllib
|
|
10
|
+
else:
|
|
11
|
+
import tomli as tomllib
|
|
5
12
|
|
|
6
13
|
from agents import detect_available_agents
|
|
7
|
-
from config import CONFIG_FILE, load_config, save_config
|
|
14
|
+
from config import CONFIG_FILE, get_default_config, load_config, save_config
|
|
8
15
|
from config.schema import validate_agent, validate_profile_reference
|
|
16
|
+
from profiles.builtins import BUILTIN_PROFILES
|
|
17
|
+
from profiles.loader import list_profile_files, load_profiles
|
|
18
|
+
from profiles.summary import format_profile_summary, sorted_profile_names
|
|
9
19
|
|
|
10
20
|
|
|
11
21
|
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
@@ -50,6 +60,63 @@ def register(subparsers: argparse._SubParsersAction) -> None:
|
|
|
50
60
|
)
|
|
51
61
|
list_parser.set_defaults(func=run_list)
|
|
52
62
|
|
|
63
|
+
# config agent
|
|
64
|
+
agent_parser = config_subparsers.add_parser(
|
|
65
|
+
"agent",
|
|
66
|
+
help="Show agent configuration",
|
|
67
|
+
description="Show default agent configuration and availability",
|
|
68
|
+
)
|
|
69
|
+
agent_parser.set_defaults(func=run_agent)
|
|
70
|
+
|
|
71
|
+
# config validation
|
|
72
|
+
validation_parser = config_subparsers.add_parser(
|
|
73
|
+
"validation",
|
|
74
|
+
help="Show validation settings",
|
|
75
|
+
description="Show validation configuration settings",
|
|
76
|
+
)
|
|
77
|
+
validation_parser.set_defaults(func=run_validation)
|
|
78
|
+
|
|
79
|
+
# config adapter
|
|
80
|
+
adapter_parser = config_subparsers.add_parser(
|
|
81
|
+
"adapter",
|
|
82
|
+
help="Show adapter settings",
|
|
83
|
+
description="Show adapter configuration settings",
|
|
84
|
+
)
|
|
85
|
+
adapter_parser.set_defaults(func=run_adapter)
|
|
86
|
+
|
|
87
|
+
# config oasr
|
|
88
|
+
oasr_parser = config_subparsers.add_parser(
|
|
89
|
+
"oasr",
|
|
90
|
+
help="Show core OASR settings",
|
|
91
|
+
description="Show core OASR configuration settings",
|
|
92
|
+
)
|
|
93
|
+
oasr_parser.set_defaults(func=run_oasr)
|
|
94
|
+
|
|
95
|
+
# config profiles
|
|
96
|
+
profiles_parser = config_subparsers.add_parser(
|
|
97
|
+
"profiles",
|
|
98
|
+
help="Show execution profiles",
|
|
99
|
+
description="Show available execution policy profiles",
|
|
100
|
+
)
|
|
101
|
+
profiles_parser.add_argument("--names", action="store_true", help="Output profile names only")
|
|
102
|
+
profiles_parser.set_defaults(func=run_profiles)
|
|
103
|
+
|
|
104
|
+
# config man
|
|
105
|
+
man_parser = config_subparsers.add_parser(
|
|
106
|
+
"man",
|
|
107
|
+
help="Show configuration reference",
|
|
108
|
+
description="Show configuration reference with examples",
|
|
109
|
+
)
|
|
110
|
+
man_parser.set_defaults(func=run_man)
|
|
111
|
+
|
|
112
|
+
# config validate
|
|
113
|
+
validate_parser = config_subparsers.add_parser(
|
|
114
|
+
"validate",
|
|
115
|
+
help="Validate config file",
|
|
116
|
+
description="Validate configuration file structure and values",
|
|
117
|
+
)
|
|
118
|
+
validate_parser.set_defaults(func=run_validate)
|
|
119
|
+
|
|
53
120
|
# config path
|
|
54
121
|
path_parser = config_subparsers.add_parser(
|
|
55
122
|
"path",
|
|
@@ -62,43 +129,82 @@ def register(subparsers: argparse._SubParsersAction) -> None:
|
|
|
62
129
|
parser.set_defaults(func=lambda args: parser.print_help() or 1)
|
|
63
130
|
|
|
64
131
|
|
|
132
|
+
def _load_file_config(config_path: Path) -> dict[str, Any]:
|
|
133
|
+
if config_path.exists():
|
|
134
|
+
with open(config_path, "rb") as f:
|
|
135
|
+
data = tomllib.load(f)
|
|
136
|
+
if isinstance(data, dict):
|
|
137
|
+
return data
|
|
138
|
+
return {}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _parse_key(key: str) -> tuple[str, str]:
|
|
142
|
+
key = key.lower()
|
|
143
|
+
aliases = {
|
|
144
|
+
"agent": ("agent", "default"),
|
|
145
|
+
"profile": ("oasr", "default_profile"),
|
|
146
|
+
}
|
|
147
|
+
if key in aliases:
|
|
148
|
+
return aliases[key]
|
|
149
|
+
if "." in key:
|
|
150
|
+
parts = key.split(".", 1)
|
|
151
|
+
if len(parts) != 2:
|
|
152
|
+
raise ValueError(f"Invalid key '{key}'. Use format 'section.field' or 'agent'")
|
|
153
|
+
return parts[0], parts[1]
|
|
154
|
+
raise ValueError(f"Invalid key '{key}'. Use format 'section.field' or 'agent'")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _format_value(value: Any) -> str:
|
|
158
|
+
if isinstance(value, bool):
|
|
159
|
+
return "true" if value else "false"
|
|
160
|
+
if isinstance(value, list):
|
|
161
|
+
return ", ".join(str(item) for item in value)
|
|
162
|
+
return str(value)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parse_bool(value: str, field: str) -> bool:
|
|
166
|
+
value_lower = value.lower()
|
|
167
|
+
if value_lower in ("true", "1", "yes", "on"):
|
|
168
|
+
return True
|
|
169
|
+
if value_lower in ("false", "0", "no", "off"):
|
|
170
|
+
return False
|
|
171
|
+
raise ValueError(f"'{field}' must be a boolean")
|
|
172
|
+
|
|
173
|
+
|
|
65
174
|
def run_set(args: argparse.Namespace) -> int:
|
|
66
175
|
"""Set a configuration value with validation."""
|
|
67
176
|
key = args.key.lower()
|
|
68
177
|
value = args.value
|
|
69
178
|
force = getattr(args, "force", False)
|
|
70
179
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
print(f"Error: Invalid key '{key}'. Use format 'section.field' or 'agent'", file=sys.stderr)
|
|
76
|
-
return 1
|
|
77
|
-
section, field = parts
|
|
78
|
-
elif key == "agent":
|
|
79
|
-
# Special case: bare "agent" means "agent.default"
|
|
80
|
-
section, field = "agent", "default"
|
|
81
|
-
else:
|
|
82
|
-
print(f"Error: Invalid key '{key}'. Use format 'section.field' or 'agent'", file=sys.stderr)
|
|
180
|
+
try:
|
|
181
|
+
section, field = _parse_key(key)
|
|
182
|
+
except ValueError as exc:
|
|
183
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
83
184
|
return 1
|
|
84
185
|
|
|
85
186
|
# Type coercion based on field
|
|
86
187
|
original_value = value
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
188
|
+
try:
|
|
189
|
+
if field in ("strict", "completions"):
|
|
190
|
+
value = _parse_bool(value, field)
|
|
191
|
+
elif field == "reference_max_lines":
|
|
91
192
|
value = int(value)
|
|
92
193
|
if value < 1:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
194
|
+
raise ValueError(f"'{field}' must be a positive integer")
|
|
195
|
+
elif field == "default_targets":
|
|
196
|
+
value = [item.strip() for item in value.split(",") if item.strip()]
|
|
197
|
+
elif field == "default_profile":
|
|
198
|
+
value = value.strip()
|
|
199
|
+
except ValueError as exc:
|
|
200
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
201
|
+
return 1
|
|
98
202
|
|
|
99
203
|
# Load config
|
|
100
204
|
config_path = getattr(args, "config", None)
|
|
205
|
+
config_file_path = config_path or CONFIG_FILE
|
|
101
206
|
config = load_config(config_path=config_path)
|
|
207
|
+
file_config = _load_file_config(config_file_path)
|
|
102
208
|
|
|
103
209
|
# Validate before setting (unless --force)
|
|
104
210
|
if not force:
|
|
@@ -115,18 +221,18 @@ def run_set(args: argparse.Namespace) -> int:
|
|
|
115
221
|
is_valid, error_msg = validate_profile_reference(value, config)
|
|
116
222
|
if not is_valid:
|
|
117
223
|
print(f"Error: {error_msg}", file=sys.stderr)
|
|
118
|
-
print("\nCreate the profile in ~/.oasr/config.toml
|
|
224
|
+
print("\nCreate the profile in ~/.oasr/config.toml or ~/.oasr/profile/, or use:", file=sys.stderr)
|
|
119
225
|
print(f" oasr config set --force oasr.default_profile {value}", file=sys.stderr)
|
|
120
226
|
return 1
|
|
121
227
|
|
|
122
228
|
# Set the value
|
|
123
|
-
if section not in
|
|
124
|
-
|
|
229
|
+
if section not in file_config:
|
|
230
|
+
file_config[section] = {}
|
|
125
231
|
|
|
126
|
-
|
|
232
|
+
file_config[section][field] = value
|
|
127
233
|
|
|
128
234
|
try:
|
|
129
|
-
save_config(
|
|
235
|
+
save_config(file_config, config_path=config_file_path)
|
|
130
236
|
|
|
131
237
|
# Show confirmation
|
|
132
238
|
if section == "agent" and field == "default":
|
|
@@ -137,6 +243,8 @@ def run_set(args: argparse.Namespace) -> int:
|
|
|
137
243
|
else:
|
|
138
244
|
print(f"✓ Default agent set to: {value}")
|
|
139
245
|
print(f" Warning: '{value}' binary not found in PATH. Install it to use this agent.", file=sys.stderr)
|
|
246
|
+
elif section == "oasr" and field == "default_profile":
|
|
247
|
+
print(f"✓ Default profile set to: {value}")
|
|
140
248
|
else:
|
|
141
249
|
print(f"✓ Set {section}.{field} = {original_value}")
|
|
142
250
|
|
|
@@ -152,18 +260,20 @@ def run_get(args: argparse.Namespace) -> int:
|
|
|
152
260
|
|
|
153
261
|
config = load_config(args.config if hasattr(args, "config") else None)
|
|
154
262
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else:
|
|
160
|
-
print("No default agent configured", file=sys.stderr)
|
|
161
|
-
return 1
|
|
162
|
-
return 0
|
|
163
|
-
else:
|
|
164
|
-
print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
|
|
263
|
+
try:
|
|
264
|
+
section, field = _parse_key(key)
|
|
265
|
+
except ValueError as exc:
|
|
266
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
165
267
|
return 1
|
|
166
268
|
|
|
269
|
+
value = config.get(section, {}).get(field)
|
|
270
|
+
if value is None:
|
|
271
|
+
print(f"Config value not set: {section}.{field}", file=sys.stderr)
|
|
272
|
+
return 1
|
|
273
|
+
|
|
274
|
+
print(_format_value(value))
|
|
275
|
+
return 0
|
|
276
|
+
|
|
167
277
|
|
|
168
278
|
def run_list(args: argparse.Namespace) -> int:
|
|
169
279
|
"""List all configuration."""
|
|
@@ -202,9 +312,146 @@ def run_list(args: argparse.Namespace) -> int:
|
|
|
202
312
|
print(f" default_targets = {config['adapter']['default_targets']}")
|
|
203
313
|
print()
|
|
204
314
|
|
|
315
|
+
# OASR section
|
|
316
|
+
print(" [oasr]")
|
|
317
|
+
print(f" default_profile = {config['oasr']['default_profile']}")
|
|
318
|
+
print(f" completions = {_format_value(config['oasr']['completions'])}")
|
|
319
|
+
print()
|
|
320
|
+
|
|
321
|
+
# Profiles section
|
|
322
|
+
print(" [profiles]")
|
|
323
|
+
profiles = config.get("profiles", {})
|
|
324
|
+
for name in sorted_profile_names(profiles):
|
|
325
|
+
profile = profiles[name]
|
|
326
|
+
summary = format_profile_summary(name, profile)
|
|
327
|
+
print(f" {summary}")
|
|
328
|
+
print()
|
|
329
|
+
|
|
330
|
+
return 0
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def run_agent(args: argparse.Namespace) -> int:
|
|
334
|
+
config = load_config(args.config if hasattr(args, "config") else None)
|
|
335
|
+
agent = config["agent"].get("default")
|
|
336
|
+
available = detect_available_agents()
|
|
337
|
+
|
|
338
|
+
print("Agent configuration:")
|
|
339
|
+
print(f" Default: {agent or '(not set)'}")
|
|
340
|
+
print()
|
|
341
|
+
if available:
|
|
342
|
+
print("Available agents:")
|
|
343
|
+
for name in sorted(available):
|
|
344
|
+
print(f" ✓ {name}")
|
|
345
|
+
else:
|
|
346
|
+
print("Available agents: (none detected)")
|
|
347
|
+
return 0
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def run_validation(args: argparse.Namespace) -> int:
|
|
351
|
+
config = load_config(args.config if hasattr(args, "config") else None)
|
|
352
|
+
validation = config.get("validation", {})
|
|
353
|
+
print("Validation settings:")
|
|
354
|
+
print(f" reference_max_lines: {validation.get('reference_max_lines')}")
|
|
355
|
+
print(f" strict: {validation.get('strict')}")
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def run_adapter(args: argparse.Namespace) -> int:
|
|
360
|
+
config = load_config(args.config if hasattr(args, "config") else None)
|
|
361
|
+
adapter = config.get("adapter", {})
|
|
362
|
+
targets = adapter.get("default_targets", [])
|
|
363
|
+
print("Adapter settings:")
|
|
364
|
+
print(f" default_targets: {', '.join(targets)}")
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def run_oasr(args: argparse.Namespace) -> int:
|
|
369
|
+
config = load_config(args.config if hasattr(args, "config") else None)
|
|
370
|
+
oasr = config.get("oasr", {})
|
|
371
|
+
print("OASR settings:")
|
|
372
|
+
print(f" default_profile: {oasr.get('default_profile')}")
|
|
373
|
+
print(f" completions: {_format_value(oasr.get('completions'))}")
|
|
205
374
|
return 0
|
|
206
375
|
|
|
207
376
|
|
|
377
|
+
def run_profiles(args: argparse.Namespace) -> int:
|
|
378
|
+
config = load_config(args.config if hasattr(args, "config") else None)
|
|
379
|
+
profiles = config.get("profiles", {})
|
|
380
|
+
names = sorted_profile_names(profiles)
|
|
381
|
+
|
|
382
|
+
if args.names:
|
|
383
|
+
print("\n".join(names))
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
print("Profiles:")
|
|
387
|
+
for name in names:
|
|
388
|
+
summary = format_profile_summary(name, profiles[name])
|
|
389
|
+
suffix = " (built-in)" if name in BUILTIN_PROFILES else ""
|
|
390
|
+
print(f" {summary}{suffix}")
|
|
391
|
+
print("\nProfile files: ~/.oasr/profile/*.toml (inline config overrides)")
|
|
392
|
+
return 0
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def run_man(args: argparse.Namespace) -> int:
|
|
396
|
+
print("OASR configuration reference")
|
|
397
|
+
print()
|
|
398
|
+
print("Quick reference:")
|
|
399
|
+
print(" oasr config set <key> <value>")
|
|
400
|
+
print(" oasr config get <key>")
|
|
401
|
+
print(" oasr config list")
|
|
402
|
+
print(" oasr config path")
|
|
403
|
+
print(" oasr config validate")
|
|
404
|
+
print()
|
|
405
|
+
print("Common keys:")
|
|
406
|
+
print(" agent")
|
|
407
|
+
print(" oasr.default_profile")
|
|
408
|
+
print(" oasr.completions")
|
|
409
|
+
print(" validation.strict")
|
|
410
|
+
print(" validation.reference_max_lines")
|
|
411
|
+
print(" adapter.default_targets")
|
|
412
|
+
print()
|
|
413
|
+
print("Examples:")
|
|
414
|
+
print(" oasr config set agent codex")
|
|
415
|
+
print(" oasr config set oasr.default_profile dev")
|
|
416
|
+
print(" oasr config set validation.strict true")
|
|
417
|
+
print(" oasr config set adapter.default_targets cursor,windsurf")
|
|
418
|
+
print()
|
|
419
|
+
print("Profiles:")
|
|
420
|
+
print(" - Built-ins: safe, strict, dev, unsafe")
|
|
421
|
+
print(" - Inline: [profiles.<name>] in config.toml")
|
|
422
|
+
print(" - Files: ~/.oasr/profile/<name>.toml (body keys only)")
|
|
423
|
+
print()
|
|
424
|
+
print("Docs: docs/commands/CONFIG.md and docs/configuration/README.md")
|
|
425
|
+
return 0
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def run_validate(args: argparse.Namespace) -> int:
|
|
429
|
+
config_path = args.config if hasattr(args, "config") and args.config else CONFIG_FILE
|
|
430
|
+
if not config_path.exists():
|
|
431
|
+
defaults = get_default_config()
|
|
432
|
+
try:
|
|
433
|
+
save_config(defaults, config_path=config_path)
|
|
434
|
+
print(f"✓ Created default config at {config_path}")
|
|
435
|
+
except ValueError as exc:
|
|
436
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
437
|
+
return 1
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
file_config = _load_file_config(config_path)
|
|
441
|
+
if not isinstance(file_config, dict):
|
|
442
|
+
raise ValueError("Config file must contain a table")
|
|
443
|
+
inline_profiles = file_config.get("profiles", {})
|
|
444
|
+
_ = load_profiles(inline_profiles=inline_profiles if isinstance(inline_profiles, dict) else {})
|
|
445
|
+
save_config(file_config, config_path=config_path)
|
|
446
|
+
print(f"✓ Config valid: {config_path}")
|
|
447
|
+
if list_profile_files():
|
|
448
|
+
print("✓ Profile files loaded")
|
|
449
|
+
return 0
|
|
450
|
+
except ValueError as exc:
|
|
451
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
452
|
+
return 1
|
|
453
|
+
|
|
454
|
+
|
|
208
455
|
def run_path(args: argparse.Namespace) -> int:
|
|
209
456
|
"""Show config file path."""
|
|
210
457
|
if hasattr(args, "config") and args.config:
|
commands/exec.py
CHANGED
|
@@ -43,6 +43,11 @@ def setup_parser(subparsers):
|
|
|
43
43
|
"--agent",
|
|
44
44
|
help="Override the default agent (codex, copilot, claude, opencode)",
|
|
45
45
|
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--unsafe",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="Pass unsafe mode flags to the agent CLI (use with caution)",
|
|
50
|
+
)
|
|
46
51
|
parser.add_argument(
|
|
47
52
|
"--profile",
|
|
48
53
|
help="Execution policy profile to use (default: from config)",
|
|
@@ -161,7 +166,22 @@ def run(args: argparse.Namespace) -> int:
|
|
|
161
166
|
print("━" * 60, file=sys.stderr)
|
|
162
167
|
|
|
163
168
|
try:
|
|
164
|
-
|
|
169
|
+
extra_args = []
|
|
170
|
+
if getattr(args, "unsafe", False):
|
|
171
|
+
if agent_name == "codex":
|
|
172
|
+
extra_args.append("--skip-git-repo-check")
|
|
173
|
+
elif agent_name == "claude":
|
|
174
|
+
extra_args.append("--dangerously-skip-permissions")
|
|
175
|
+
else:
|
|
176
|
+
print(
|
|
177
|
+
f"Warning: --unsafe is not supported for agent '{agent_name}'.",
|
|
178
|
+
file=sys.stderr,
|
|
179
|
+
)
|
|
180
|
+
print(
|
|
181
|
+
"See agent docs for trusted directory or permission configuration.",
|
|
182
|
+
file=sys.stderr,
|
|
183
|
+
)
|
|
184
|
+
result = driver.execute(skill_content, user_prompt, extra_args=extra_args or None)
|
|
165
185
|
# CompletedProcess has returncode attribute (0 = success)
|
|
166
186
|
# Output was already streamed to stdout since capture_output=False
|
|
167
187
|
return result.returncode
|
commands/profile.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""`oasr profile` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import questionary
|
|
10
|
+
|
|
11
|
+
from config import CONFIG_FILE, load_config, save_config
|
|
12
|
+
from profiles import BUILTIN_PROFILES, format_profile_summary, sorted_profile_names
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
+
parser = subparsers.add_parser(
|
|
17
|
+
"profile",
|
|
18
|
+
help="List or set execution profiles",
|
|
19
|
+
description="List and select execution policy profiles",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("name", nargs="?", help="Profile name to set as default")
|
|
22
|
+
parser.set_defaults(func=run)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _print_profiles(profiles: dict[str, dict[str, object]], current: str) -> None:
|
|
26
|
+
names = sorted_profile_names(profiles)
|
|
27
|
+
print("Profiles:")
|
|
28
|
+
for name in names:
|
|
29
|
+
summary = format_profile_summary(name, profiles[name])
|
|
30
|
+
suffix = []
|
|
31
|
+
if name == current:
|
|
32
|
+
suffix.append("current")
|
|
33
|
+
if name in BUILTIN_PROFILES:
|
|
34
|
+
suffix.append("built-in")
|
|
35
|
+
suffix_text = f" ({', '.join(suffix)})" if suffix else ""
|
|
36
|
+
print(f" {summary}{suffix_text}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _select_profile(names: list[str], current: str) -> str | None:
|
|
40
|
+
default_choice = current if current in names else (names[0] if names else None)
|
|
41
|
+
if not default_choice:
|
|
42
|
+
return None
|
|
43
|
+
response = questionary.select(
|
|
44
|
+
"Select a default profile:",
|
|
45
|
+
choices=names,
|
|
46
|
+
default=default_choice,
|
|
47
|
+
).ask()
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _set_default_profile(config_path: Path, profile_name: str) -> int:
|
|
52
|
+
config = load_config(config_path=config_path)
|
|
53
|
+
profiles = config.get("profiles", {})
|
|
54
|
+
if profile_name not in profiles:
|
|
55
|
+
print(f"Error: Profile '{profile_name}' not found.", file=sys.stderr)
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
config["oasr"]["default_profile"] = profile_name
|
|
59
|
+
save_config(config, config_path=config_path)
|
|
60
|
+
print(f"✓ Default profile set to: {profile_name}")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run(args: argparse.Namespace) -> int:
|
|
65
|
+
config_path = CONFIG_FILE
|
|
66
|
+
config = load_config(config_path=config_path)
|
|
67
|
+
profiles = config.get("profiles", {})
|
|
68
|
+
current = config.get("oasr", {}).get("default_profile", "safe")
|
|
69
|
+
|
|
70
|
+
if args.name:
|
|
71
|
+
return _set_default_profile(config_path, args.name)
|
|
72
|
+
|
|
73
|
+
if not sys.stdout.isatty() or not sys.stdin.isatty():
|
|
74
|
+
_print_profiles(profiles, current)
|
|
75
|
+
print("\nTip: run `oasr profile <name>` to set the default profile.", file=sys.stderr)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
names = sorted_profile_names(profiles)
|
|
79
|
+
choice = _select_profile(names, current)
|
|
80
|
+
if not choice:
|
|
81
|
+
print("No profiles available.", file=sys.stderr)
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
return _set_default_profile(config_path, choice)
|