oasr 0.5.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.
- __init__.py +3 -0
- __main__.py +6 -0
- adapter.py +396 -0
- adapters/__init__.py +17 -0
- adapters/base.py +254 -0
- adapters/claude.py +82 -0
- adapters/codex.py +84 -0
- adapters/copilot.py +210 -0
- adapters/cursor.py +78 -0
- adapters/windsurf.py +83 -0
- 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 +97 -0
- commands/__init__.py +6 -0
- commands/adapter.py +102 -0
- commands/add.py +435 -0
- commands/clean.py +30 -0
- commands/clone.py +178 -0
- commands/config.py +163 -0
- commands/diff.py +180 -0
- commands/exec.py +245 -0
- commands/find.py +56 -0
- commands/help.py +51 -0
- commands/info.py +152 -0
- commands/list.py +110 -0
- commands/registry.py +447 -0
- commands/rm.py +128 -0
- commands/status.py +119 -0
- commands/sync.py +143 -0
- commands/update.py +417 -0
- commands/use.py +45 -0
- commands/validate.py +74 -0
- config/__init__.py +119 -0
- config/defaults.py +40 -0
- config/schema.py +73 -0
- discovery.py +145 -0
- manifest.py +437 -0
- oasr-0.5.0.dist-info/METADATA +358 -0
- oasr-0.5.0.dist-info/RECORD +59 -0
- oasr-0.5.0.dist-info/WHEEL +4 -0
- oasr-0.5.0.dist-info/entry_points.txt +3 -0
- oasr-0.5.0.dist-info/licenses/LICENSE +187 -0
- oasr-0.5.0.dist-info/licenses/NOTICE +8 -0
- policy/__init__.py +50 -0
- policy/defaults.py +27 -0
- policy/enforcement.py +98 -0
- policy/profile.py +185 -0
- registry.py +173 -0
- remote.py +482 -0
- skillcopy/__init__.py +71 -0
- skillcopy/local.py +40 -0
- skillcopy/remote.py +98 -0
- tracking.py +181 -0
- validate.py +362 -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/diff.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""`oasr diff` command - show tracked skill status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from manifest import load_manifest
|
|
11
|
+
from tracking import extract_metadata
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(subparsers) -> None:
|
|
15
|
+
"""Register the diff command."""
|
|
16
|
+
p = subparsers.add_parser("diff", help="Show status of tracked skills (copied with metadata)")
|
|
17
|
+
p.add_argument(
|
|
18
|
+
"path",
|
|
19
|
+
nargs="?",
|
|
20
|
+
type=Path,
|
|
21
|
+
default=Path.cwd(),
|
|
22
|
+
help="Path to scan for tracked skills (default: current directory)",
|
|
23
|
+
)
|
|
24
|
+
p.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
25
|
+
p.add_argument("--quiet", action="store_true", help="Suppress info/warnings")
|
|
26
|
+
p.set_defaults(func=run)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run(args: argparse.Namespace) -> int:
|
|
30
|
+
"""Show status of tracked skills in the given path."""
|
|
31
|
+
scan_path = args.path.resolve()
|
|
32
|
+
|
|
33
|
+
if not scan_path.exists():
|
|
34
|
+
print(f"Error: Path does not exist: {scan_path}", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
# Find all SKILL.md files recursively
|
|
38
|
+
if not args.quiet and not args.json:
|
|
39
|
+
print(f"Scanning {scan_path} for tracked skills...", file=sys.stderr)
|
|
40
|
+
|
|
41
|
+
tracked_skills = []
|
|
42
|
+
skill_md_files = list(scan_path.rglob("SKILL.md"))
|
|
43
|
+
|
|
44
|
+
for skill_md in skill_md_files:
|
|
45
|
+
skill_dir = skill_md.parent
|
|
46
|
+
metadata = extract_metadata(skill_dir)
|
|
47
|
+
|
|
48
|
+
if metadata:
|
|
49
|
+
tracked_skills.append((skill_dir, metadata))
|
|
50
|
+
|
|
51
|
+
if not tracked_skills:
|
|
52
|
+
if args.json:
|
|
53
|
+
print(json.dumps({"tracked": 0, "skills": []}))
|
|
54
|
+
else:
|
|
55
|
+
print("No tracked skills found.")
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
# Determine status for each tracked skill
|
|
59
|
+
results = []
|
|
60
|
+
up_to_date = 0
|
|
61
|
+
outdated = 0
|
|
62
|
+
modified = 0
|
|
63
|
+
untracked = 0
|
|
64
|
+
|
|
65
|
+
for skill_dir, metadata in tracked_skills:
|
|
66
|
+
skill_name = skill_dir.name
|
|
67
|
+
tracked_hash = metadata.get("hash")
|
|
68
|
+
tracked_source = metadata.get("source")
|
|
69
|
+
|
|
70
|
+
# Validate metadata structure
|
|
71
|
+
if not tracked_hash or not tracked_source:
|
|
72
|
+
untracked += 1
|
|
73
|
+
results.append(
|
|
74
|
+
{
|
|
75
|
+
"name": skill_name,
|
|
76
|
+
"path": str(skill_dir),
|
|
77
|
+
"status": "error",
|
|
78
|
+
"message": "Corrupted metadata (missing hash or source)",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
if not args.quiet and not args.json:
|
|
82
|
+
print(f" ✗ {skill_name}: corrupted metadata", file=sys.stderr)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Check if in registry
|
|
86
|
+
from registry import load_registry
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
entries = load_registry()
|
|
90
|
+
except Exception as e:
|
|
91
|
+
if not args.quiet and not args.json:
|
|
92
|
+
print(f"Error loading registry: {e}", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
entry = next((e for e in entries if e.name == skill_name), None)
|
|
96
|
+
|
|
97
|
+
if entry:
|
|
98
|
+
try:
|
|
99
|
+
manifest = load_manifest(skill_name)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
untracked += 1
|
|
102
|
+
results.append(
|
|
103
|
+
{"name": skill_name, "path": str(skill_dir), "status": "error", "message": f"Manifest error: {e}"}
|
|
104
|
+
)
|
|
105
|
+
if not args.quiet and not args.json:
|
|
106
|
+
print(f" ✗ {skill_name}: manifest error", file=sys.stderr)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if manifest:
|
|
110
|
+
# Compare tracked hash with registry hash
|
|
111
|
+
if manifest.content_hash == tracked_hash:
|
|
112
|
+
# Up to date
|
|
113
|
+
status = "up-to-date"
|
|
114
|
+
up_to_date += 1
|
|
115
|
+
message = "Current"
|
|
116
|
+
else:
|
|
117
|
+
# Registry has changed
|
|
118
|
+
status = "outdated"
|
|
119
|
+
outdated += 1
|
|
120
|
+
message = "Registry has newer version"
|
|
121
|
+
else:
|
|
122
|
+
status = "untracked"
|
|
123
|
+
untracked += 1
|
|
124
|
+
message = "No manifest"
|
|
125
|
+
else:
|
|
126
|
+
status = "untracked"
|
|
127
|
+
untracked += 1
|
|
128
|
+
message = "Not in registry"
|
|
129
|
+
|
|
130
|
+
results.append(
|
|
131
|
+
{
|
|
132
|
+
"name": skill_name,
|
|
133
|
+
"path": str(skill_dir),
|
|
134
|
+
"status": status,
|
|
135
|
+
"message": message,
|
|
136
|
+
"tracked_hash": tracked_hash[:16] + "..." if tracked_hash else None,
|
|
137
|
+
"source": tracked_source,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if args.json:
|
|
142
|
+
print(
|
|
143
|
+
json.dumps(
|
|
144
|
+
{
|
|
145
|
+
"tracked": len(tracked_skills),
|
|
146
|
+
"up_to_date": up_to_date,
|
|
147
|
+
"outdated": outdated,
|
|
148
|
+
"modified": modified,
|
|
149
|
+
"untracked": untracked,
|
|
150
|
+
"skills": results,
|
|
151
|
+
},
|
|
152
|
+
indent=2,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
# Git-style output
|
|
157
|
+
for result in results:
|
|
158
|
+
if result["status"] == "up-to-date":
|
|
159
|
+
symbol = "✓"
|
|
160
|
+
elif result["status"] == "outdated":
|
|
161
|
+
symbol = "⚠"
|
|
162
|
+
elif result["status"] == "modified":
|
|
163
|
+
symbol = "✗"
|
|
164
|
+
else:
|
|
165
|
+
symbol = "?"
|
|
166
|
+
|
|
167
|
+
print(f"{symbol} {result['name']}: {result['status']}")
|
|
168
|
+
if not args.quiet:
|
|
169
|
+
print(f" Path: {result['path']}")
|
|
170
|
+
if result["message"]:
|
|
171
|
+
print(f" {result['message']}")
|
|
172
|
+
|
|
173
|
+
print(
|
|
174
|
+
f"\n{len(tracked_skills)} tracked: {up_to_date} up-to-date, {outdated} outdated, {modified} modified, {untracked} untracked"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if outdated > 0:
|
|
178
|
+
print("\nRun 'oasr sync' to update outdated skills.")
|
|
179
|
+
|
|
180
|
+
return 1 if outdated > 0 or modified > 0 else 0
|
commands/exec.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
import policy
|
|
8
|
+
from agents.registry import detect_available_agents, get_driver
|
|
9
|
+
from config import load_config
|
|
10
|
+
from registry import load_registry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(subparsers):
|
|
14
|
+
"""Register the exec command."""
|
|
15
|
+
setup_parser(subparsers)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def setup_parser(subparsers):
|
|
19
|
+
"""Set up the exec command parser."""
|
|
20
|
+
parser = subparsers.add_parser(
|
|
21
|
+
"exec",
|
|
22
|
+
help="Execute a skill from the registry",
|
|
23
|
+
description="Execute a skill from the registry using an agent CLI. "
|
|
24
|
+
"The skill is executed in the current working directory.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"skill_name",
|
|
28
|
+
help="Name of the skill to execute from the registry",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"-p",
|
|
32
|
+
"--prompt",
|
|
33
|
+
help="Inline prompt/instructions for the agent",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-i",
|
|
37
|
+
"--instructions",
|
|
38
|
+
metavar="FILE",
|
|
39
|
+
help="Read prompt/instructions from a file",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"-a",
|
|
43
|
+
"--agent",
|
|
44
|
+
help="Override the default agent (codex, copilot, claude, opencode)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--profile",
|
|
48
|
+
help="Execution policy profile to use (default: from config)",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"-y",
|
|
52
|
+
"--yes",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="Skip confirmation prompt for risky operations",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--confirm",
|
|
58
|
+
action="store_true",
|
|
59
|
+
help="Force confirmation prompt even for safe operations",
|
|
60
|
+
)
|
|
61
|
+
parser.set_defaults(func=run)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run(args: argparse.Namespace) -> int:
|
|
65
|
+
"""Execute a skill from the registry."""
|
|
66
|
+
# Load registry to find the skill
|
|
67
|
+
entries = load_registry()
|
|
68
|
+
entry_map = {e.name: e for e in entries}
|
|
69
|
+
skill_name = args.skill_name
|
|
70
|
+
|
|
71
|
+
if skill_name not in entry_map:
|
|
72
|
+
print(f"Error: Skill '{skill_name}' not found in registry", file=sys.stderr)
|
|
73
|
+
print("\nUse 'oasr registry list' to see available skills.", file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
skill_entry = entry_map[skill_name]
|
|
77
|
+
skill_source = skill_entry.path
|
|
78
|
+
|
|
79
|
+
if not skill_source:
|
|
80
|
+
print(f"Error: Skill '{skill_name}' has no source configured", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
# Get the skill content - look for SKILL.md in the skill directory
|
|
84
|
+
skill_dir = Path(skill_source)
|
|
85
|
+
skill_path = skill_dir / "SKILL.md"
|
|
86
|
+
|
|
87
|
+
if not skill_path.exists():
|
|
88
|
+
print(f"Error: Skill file not found: {skill_path}", file=sys.stderr)
|
|
89
|
+
print("\nTry running 'oasr sync' to update your skills.", file=sys.stderr)
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
skill_content = skill_path.read_text(encoding="utf-8")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Error reading skill file: {e}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
# Get the user prompt from various sources
|
|
99
|
+
user_prompt = _get_user_prompt(args)
|
|
100
|
+
if user_prompt is None:
|
|
101
|
+
# Error already printed by _get_user_prompt
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
# Determine which agent to use
|
|
105
|
+
agent_name = _get_agent_name(args)
|
|
106
|
+
if agent_name is None:
|
|
107
|
+
# Error already printed by _get_agent_name
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
# === POLICY ENFORCEMENT ===
|
|
111
|
+
# Load configuration
|
|
112
|
+
config = load_config()
|
|
113
|
+
|
|
114
|
+
# Determine which profile to use (flag overrides config)
|
|
115
|
+
profile_name = args.profile if args.profile else config.get("oasr", {}).get("default_profile", "safe")
|
|
116
|
+
|
|
117
|
+
# Load the policy profile
|
|
118
|
+
profile = policy.load(config, profile_name)
|
|
119
|
+
|
|
120
|
+
# Detect execution context for risk assessment
|
|
121
|
+
stdin_used = not sys.stdin.isatty() and not args.prompt and not args.instructions
|
|
122
|
+
instructions_file_used = bool(args.instructions)
|
|
123
|
+
|
|
124
|
+
# Assess risk and determine if confirmation is needed
|
|
125
|
+
needs_confirm, reasons = policy.assess_risk(
|
|
126
|
+
profile=profile,
|
|
127
|
+
stdin_used=stdin_used,
|
|
128
|
+
instructions_file_used=instructions_file_used,
|
|
129
|
+
force_confirm=args.confirm,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Require confirmation unless --yes flag is present
|
|
133
|
+
if needs_confirm and not args.yes:
|
|
134
|
+
summary = policy.summarize(profile, skill_name, agent_name)
|
|
135
|
+
if not policy.prompt_confirmation(summary, reasons):
|
|
136
|
+
return 1 # User aborted
|
|
137
|
+
|
|
138
|
+
# === END POLICY ENFORCEMENT ===
|
|
139
|
+
|
|
140
|
+
# Get the agent driver
|
|
141
|
+
try:
|
|
142
|
+
driver = get_driver(agent_name)
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
145
|
+
return 1
|
|
146
|
+
|
|
147
|
+
# Execute the skill
|
|
148
|
+
print(f"Executing skill '{skill_name}' with {agent_name}...", file=sys.stderr)
|
|
149
|
+
print("━" * 60, file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = driver.execute(skill_content, user_prompt)
|
|
153
|
+
# CompletedProcess has returncode attribute (0 = success)
|
|
154
|
+
# Output was already streamed to stdout since capture_output=False
|
|
155
|
+
return result.returncode
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"\nUnexpected error: {e}", file=sys.stderr)
|
|
158
|
+
return 1
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _get_user_prompt(args: argparse.Namespace) -> str | None:
|
|
162
|
+
"""Get the user prompt from CLI args or stdin.
|
|
163
|
+
|
|
164
|
+
Returns None if there's an error, with error message printed to stderr.
|
|
165
|
+
"""
|
|
166
|
+
# Check for conflicting options
|
|
167
|
+
if args.prompt and args.instructions:
|
|
168
|
+
print(
|
|
169
|
+
"Error: Cannot use both --prompt and --instructions at the same time",
|
|
170
|
+
file=sys.stderr,
|
|
171
|
+
)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# Option 1: Inline prompt via -p/--prompt
|
|
175
|
+
if args.prompt:
|
|
176
|
+
return args.prompt
|
|
177
|
+
|
|
178
|
+
# Option 2: File-based instructions via -i/--instructions
|
|
179
|
+
if args.instructions:
|
|
180
|
+
instructions_path = Path(args.instructions)
|
|
181
|
+
if not instructions_path.exists():
|
|
182
|
+
print(f"Error: Instructions file not found: {args.instructions}", file=sys.stderr)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
return instructions_path.read_text(encoding="utf-8")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"Error reading instructions file: {e}", file=sys.stderr)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Option 3: Read from stdin
|
|
192
|
+
if not sys.stdin.isatty():
|
|
193
|
+
try:
|
|
194
|
+
return sys.stdin.read()
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"Error reading from stdin: {e}", file=sys.stderr)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# No prompt provided
|
|
200
|
+
print("Error: No prompt provided", file=sys.stderr)
|
|
201
|
+
print("\nProvide a prompt using one of:", file=sys.stderr)
|
|
202
|
+
print(" -p/--prompt 'Your prompt here'", file=sys.stderr)
|
|
203
|
+
print(" -i/--instructions path/to/file.txt", file=sys.stderr)
|
|
204
|
+
print(" echo 'Your prompt' | oasr exec <skill>", file=sys.stderr)
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _get_agent_name(args: argparse.Namespace) -> str | None:
|
|
209
|
+
"""Get the agent name from CLI flag or config.
|
|
210
|
+
|
|
211
|
+
Returns None if there's an error, with error message printed to stderr.
|
|
212
|
+
"""
|
|
213
|
+
# Option 1: Explicit --agent flag
|
|
214
|
+
if args.agent:
|
|
215
|
+
agent_name = args.agent.lower()
|
|
216
|
+
# Validate it's a known and available agent
|
|
217
|
+
available = detect_available_agents()
|
|
218
|
+
if agent_name not in available or not available[agent_name]:
|
|
219
|
+
print(f"Error: Agent '{agent_name}' is not available", file=sys.stderr)
|
|
220
|
+
print("\nAvailable agents:", file=sys.stderr)
|
|
221
|
+
for name in sorted(available.keys()):
|
|
222
|
+
status = "✓" if available[name] else "✗"
|
|
223
|
+
print(f" {status} {name}", file=sys.stderr)
|
|
224
|
+
return None
|
|
225
|
+
return agent_name
|
|
226
|
+
|
|
227
|
+
# Option 2: Default from config
|
|
228
|
+
config = load_config()
|
|
229
|
+
default_agent = config.get("agent", {}).get("default")
|
|
230
|
+
|
|
231
|
+
if default_agent:
|
|
232
|
+
return default_agent
|
|
233
|
+
|
|
234
|
+
# No agent configured
|
|
235
|
+
print("Error: No agent configured", file=sys.stderr)
|
|
236
|
+
print("\nConfigure a default agent with:", file=sys.stderr)
|
|
237
|
+
print(" oasr config set agent <agent-name>", file=sys.stderr)
|
|
238
|
+
print("\nOr specify an agent for this command:", file=sys.stderr)
|
|
239
|
+
print(" oasr exec --agent <agent-name> <skill>", file=sys.stderr)
|
|
240
|
+
print("\nAvailable agents:", file=sys.stderr)
|
|
241
|
+
available = detect_available_agents()
|
|
242
|
+
for name in sorted(available.keys()):
|
|
243
|
+
status = "✓" if available[name] else "✗"
|
|
244
|
+
print(f" {status} {name}", file=sys.stderr)
|
|
245
|
+
return None
|
commands/find.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""`asr find` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from discovery import find_skills
|
|
11
|
+
from registry import SkillEntry, add_skill
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(subparsers) -> None:
|
|
15
|
+
p = subparsers.add_parser("find", help="Find skills recursively")
|
|
16
|
+
p.add_argument("root", type=Path, help="Root directory to search")
|
|
17
|
+
p.add_argument("--add", action="store_true", dest="add_found", help="Register found skills")
|
|
18
|
+
p.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
19
|
+
p.add_argument("--quiet", action="store_true", help="Suppress info/warnings")
|
|
20
|
+
p.set_defaults(func=run)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(args: argparse.Namespace) -> int:
|
|
24
|
+
root = args.root.resolve()
|
|
25
|
+
|
|
26
|
+
if not root.is_dir():
|
|
27
|
+
print(f"Error: Not a directory: {root}", file=sys.stderr)
|
|
28
|
+
return 2
|
|
29
|
+
|
|
30
|
+
skills = find_skills(root)
|
|
31
|
+
|
|
32
|
+
if args.json:
|
|
33
|
+
data = [{"name": s.name, "description": s.description, "path": str(s.path)} for s in skills]
|
|
34
|
+
print(json.dumps(data, indent=2))
|
|
35
|
+
else:
|
|
36
|
+
if not skills:
|
|
37
|
+
print(f"No skills found under {root}")
|
|
38
|
+
else:
|
|
39
|
+
for s in skills:
|
|
40
|
+
print(f"{s.name:<30} {s.path}")
|
|
41
|
+
|
|
42
|
+
if args.add_found and skills:
|
|
43
|
+
added = 0
|
|
44
|
+
for s in skills:
|
|
45
|
+
entry = SkillEntry(
|
|
46
|
+
path=str(s.path),
|
|
47
|
+
name=s.name,
|
|
48
|
+
description=s.description,
|
|
49
|
+
)
|
|
50
|
+
if add_skill(entry):
|
|
51
|
+
added += 1
|
|
52
|
+
|
|
53
|
+
if not args.json and not args.quiet:
|
|
54
|
+
print(f"\nRegistered {added} new skill(s), {len(skills) - added} updated.")
|
|
55
|
+
|
|
56
|
+
return 0
|
commands/help.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""`asr help` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(subparsers, parser_ref: argparse.ArgumentParser) -> None:
|
|
9
|
+
"""Register the help subcommand.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
subparsers: The subparsers object to add to.
|
|
13
|
+
parser_ref: Reference to the main parser for displaying help.
|
|
14
|
+
"""
|
|
15
|
+
p = subparsers.add_parser(
|
|
16
|
+
"help",
|
|
17
|
+
help="Show help for a command",
|
|
18
|
+
add_help=False,
|
|
19
|
+
)
|
|
20
|
+
p.add_argument(
|
|
21
|
+
"command",
|
|
22
|
+
nargs="?",
|
|
23
|
+
help="Command to show help for",
|
|
24
|
+
)
|
|
25
|
+
p.set_defaults(func=lambda args: run(args, parser_ref))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
29
|
+
"""Show help for the specified command or general help."""
|
|
30
|
+
if not args.command:
|
|
31
|
+
parser.print_help()
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
# Find the subparser for the given command
|
|
35
|
+
subparsers_action = None
|
|
36
|
+
for action in parser._actions:
|
|
37
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
38
|
+
subparsers_action = action
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
if subparsers_action is None:
|
|
42
|
+
print("Error: No commands available")
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
if args.command in subparsers_action.choices:
|
|
46
|
+
subparsers_action.choices[args.command].print_help()
|
|
47
|
+
return 0
|
|
48
|
+
else:
|
|
49
|
+
print(f"Unknown command: {args.command}")
|
|
50
|
+
print(f"\nAvailable commands: {', '.join(sorted(subparsers_action.choices.keys()))}")
|
|
51
|
+
return 1
|