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.
- adapters/base.py +4 -4
- agents/__init__.py +25 -0
- agents/base.py +96 -0
- agents/claude.py +25 -0
- agents/codex.py +25 -0
- agents/copilot.py +25 -0
- agents/opencode.py +25 -0
- agents/registry.py +57 -0
- cli.py +8 -5
- commands/add.py +137 -4
- commands/clone.py +178 -0
- commands/config.py +163 -0
- commands/exec.py +199 -0
- commands/use.py +17 -144
- config.py → config/__init__.py +40 -17
- config/defaults.py +16 -0
- config/schema.py +36 -0
- {oasr-0.3.4.dist-info → oasr-0.4.1.dist-info}/METADATA +1 -1
- {oasr-0.3.4.dist-info → oasr-0.4.1.dist-info}/RECORD +23 -11
- {oasr-0.3.4.dist-info → oasr-0.4.1.dist-info}/WHEEL +0 -0
- {oasr-0.3.4.dist-info → oasr-0.4.1.dist-info}/entry_points.txt +0 -0
- {oasr-0.3.4.dist-info → oasr-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {oasr-0.3.4.dist-info → oasr-0.4.1.dist-info}/licenses/NOTICE +0 -0
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
|
|
12
|
-
from skillcopy import copy_skill
|
|
9
|
+
from commands import clone
|
|
13
10
|
|
|
14
11
|
|
|
15
12
|
def register(subparsers) -> None:
|
|
16
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
44
|
+
# Delegate to clone command
|
|
45
|
+
return clone.run(args)
|
config.py → config/__init__.py
RENAMED
|
@@ -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
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
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
|
-
|
|
52
|
-
config
|
|
53
|
-
|
|
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(
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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")
|