agents-tool 0.1.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.
@@ -0,0 +1,3 @@
1
+ """Agents Tool - CLI for managing AI agents, skills and harness."""
2
+
3
+ __version__ = "0.1.0"
agents_tool/cli.py ADDED
@@ -0,0 +1,36 @@
1
+ """CLI entry point for agents-tool."""
2
+
3
+ import click
4
+
5
+ from . import __version__
6
+
7
+
8
+ @click.group()
9
+ @click.version_option(version=__version__)
10
+ def main():
11
+ """Agents Tool - Manage AI agents, skills and harness."""
12
+ pass
13
+
14
+
15
+ @main.command()
16
+ def init():
17
+ """Initialize agents-tool configuration."""
18
+ from .config import init_config
19
+
20
+ init_config()
21
+
22
+
23
+ @main.command()
24
+ def gou():
25
+ """Get or update agents from configured repos."""
26
+ from .git import process_repos
27
+
28
+ process_repos()
29
+
30
+
31
+ @main.command()
32
+ def list():
33
+ """List installed agents, skills and harness."""
34
+ from .registry import list_installed
35
+
36
+ list_installed()
agents_tool/config.py ADDED
@@ -0,0 +1,59 @@
1
+ """Config management for agents-tool."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import click
7
+ import yaml
8
+
9
+ # Local config (in current directory)
10
+ CONFIG_DIR = Path.cwd() / ".agents"
11
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
12
+ LOCAL_DIR = Path.cwd() / ".agents"
13
+ REPOS_DIR = LOCAL_DIR / "repos"
14
+ INSTALLED_DIR = LOCAL_DIR / "installed"
15
+
16
+ CONFIG_TEMPLATE = """\
17
+ # Agents Tool Configuration
18
+ # Edit this file with your repos and preferences.
19
+
20
+ repos:
21
+ # Example:
22
+ # - url: git@github.com:your-username/your-agents.git
23
+ # user: your-username
24
+ # branch: main
25
+ #
26
+ # - url: git@github.com:team/shared-agents.git
27
+ # user: your-username
28
+ # branch: develop
29
+
30
+ defaults:
31
+ branch: main
32
+ """
33
+
34
+
35
+ def init_config():
36
+ """Initialize configuration and directories."""
37
+ if CONFIG_FILE.exists():
38
+ click.echo(f"Config already exists: {CONFIG_FILE}")
39
+ click.echo("Edit it manually to add your repos.")
40
+ return
41
+
42
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
43
+ REPOS_DIR.mkdir(parents=True, exist_ok=True)
44
+ INSTALLED_DIR.mkdir(parents=True, exist_ok=True)
45
+
46
+ CONFIG_FILE.write_text(CONFIG_TEMPLATE)
47
+
48
+ click.echo("Agents tool initialized!")
49
+ click.echo(f"Config: {CONFIG_FILE}")
50
+ click.echo("Edit it to add your repos, then run: agents gou")
51
+
52
+
53
+ def load_config():
54
+ """Load configuration from YAML file."""
55
+ if not CONFIG_FILE.exists():
56
+ raise FileNotFoundError(f"Config not found: {CONFIG_FILE}")
57
+
58
+ with open(CONFIG_FILE) as f:
59
+ return yaml.safe_load(f)
agents_tool/git.py ADDED
@@ -0,0 +1,95 @@
1
+ """Git operations for agents-tool."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from git import Repo
8
+
9
+ from .config import load_config, REPOS_DIR, INSTALLED_DIR
10
+ from .registry import sync_installed, update_registry
11
+
12
+
13
+ def process_repos():
14
+ """Process all repos from config (clone or update)."""
15
+ config = load_config()
16
+ repos = config.get("repos", [])
17
+
18
+ if not repos:
19
+ click.echo("No repos configured.")
20
+ click.echo("Edit .agents/config.yaml to add repos.")
21
+ return
22
+
23
+ for repo_config in repos:
24
+ process_repo(repo_config)
25
+
26
+ # Sync and update registry after all repos are processed
27
+ click.echo()
28
+ click.echo("Syncing installed...")
29
+ sync_installed()
30
+
31
+ click.echo("Updating registry...")
32
+ update_registry()
33
+
34
+ click.echo()
35
+ click.echo("Done!")
36
+
37
+
38
+ def process_repo(repo_config):
39
+ """Process a single repo (clone or update)."""
40
+ url = repo_config.get("url")
41
+ user = repo_config.get("user", "")
42
+ branch = repo_config.get("branch", "main")
43
+
44
+ if not url:
45
+ click.echo("Warning: Repo without URL, skipping.")
46
+ return
47
+
48
+ repo_name = extract_repo_name(url)
49
+ repo_path = REPOS_DIR / repo_name
50
+
51
+ if repo_path.exists():
52
+ update_repo(repo_path, branch)
53
+ else:
54
+ clone_repo(url, repo_path, branch)
55
+
56
+
57
+ def extract_repo_name(url):
58
+ """Extract repo name from git URL."""
59
+ # Handle SSH URLs
60
+ if url.endswith(".git"):
61
+ url = url[:-4]
62
+ # Get last part after /
63
+ return url.split("/")[-1]
64
+
65
+
66
+ def clone_repo(url, path, branch):
67
+ """Clone a repo with authentication."""
68
+ click.echo(f"Cloning: {url}")
69
+
70
+ try:
71
+ # Try cloning without auth first (public repos)
72
+ Repo.clone_from(url, str(path), branch=branch)
73
+ click.echo(f" Cloned to: {path}")
74
+ except Exception as e:
75
+ if "Authentication" in str(e) or "public" in str(e).lower():
76
+ # Need auth - ask for token
77
+ token = click.prompt("Token", hide_input=True)
78
+ auth_url = url.replace("git@", f"https://{token}@")
79
+ Repo.clone_from(auth_url, str(path), branch=branch)
80
+ click.echo(f" Cloned to: {path}")
81
+ else:
82
+ click.echo(f" Error: {e}")
83
+
84
+
85
+ def update_repo(path, branch):
86
+ """Pull updates for an existing repo."""
87
+ click.echo(f"Updating: {path.name}")
88
+
89
+ try:
90
+ repo = Repo(str(path))
91
+ origin = repo.remotes.origin
92
+ origin.pull(branch)
93
+ click.echo(" Updated")
94
+ except Exception as e:
95
+ click.echo(f" Error: {e}")
@@ -0,0 +1,229 @@
1
+ """Registry for installed agents, skills and harness."""
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import yaml
9
+
10
+ from .config import REPOS_DIR, INSTALLED_DIR
11
+ from .validator import validate_agent_yaml, validate_skill_md
12
+
13
+ REGISTRY_FILE = INSTALLED_DIR / "registry.json"
14
+
15
+
16
+ def update_registry():
17
+ """Update registry.json with current installed content."""
18
+ INSTALLED_DIR.mkdir(parents=True, exist_ok=True)
19
+
20
+ registry = {
21
+ "agents": find_agents(),
22
+ "skills": find_skills(),
23
+ "harness": find_harness(),
24
+ }
25
+
26
+ with open(REGISTRY_FILE, "w") as f:
27
+ json.dump(registry, f, indent=2)
28
+
29
+ return registry
30
+
31
+
32
+ def load_registry():
33
+ """Load registry from file."""
34
+ if not REGISTRY_FILE.exists():
35
+ return update_registry()
36
+
37
+ with open(REGISTRY_FILE) as f:
38
+ return json.load(f)
39
+
40
+
41
+ def sync_installed():
42
+ """Sync repos content to installed directory."""
43
+ INSTALLED_DIR.mkdir(parents=True, exist_ok=True)
44
+
45
+ # Clean installed directory
46
+ for item in INSTALLED_DIR.iterdir():
47
+ if item.name == "registry.json":
48
+ continue
49
+ if item.is_dir():
50
+ shutil.rmtree(item)
51
+ else:
52
+ item.unlink()
53
+
54
+ agents_dir = INSTALLED_DIR / "agents"
55
+ skills_dir = INSTALLED_DIR / "skills"
56
+ harness_dir = INSTALLED_DIR / "harness"
57
+
58
+ agents_dir.mkdir(exist_ok=True)
59
+ skills_dir.mkdir(exist_ok=True)
60
+ harness_dir.mkdir(exist_ok=True)
61
+
62
+ if not REPOS_DIR.exists():
63
+ return
64
+
65
+ for repo_dir in REPOS_DIR.iterdir():
66
+ if not repo_dir.is_dir():
67
+ continue
68
+
69
+ # Sync agents
70
+ repo_agents = repo_dir / "agents"
71
+ if repo_agents.exists():
72
+ for agent_dir in repo_agents.iterdir():
73
+ if not agent_dir.is_dir():
74
+ continue
75
+
76
+ is_valid, errors, config = validate_agent_yaml(agent_dir)
77
+ if is_valid:
78
+ dest = agents_dir / agent_dir.name
79
+ if not dest.exists():
80
+ shutil.copytree(agent_dir, dest)
81
+ else:
82
+ click.echo(f"Warning: Invalid agent {agent_dir.name}: {errors}")
83
+
84
+ # Sync skills
85
+ repo_skills = repo_dir / "skills"
86
+ if repo_skills.exists():
87
+ for skill_dir in repo_skills.iterdir():
88
+ if not skill_dir.is_dir():
89
+ continue
90
+
91
+ is_valid, errors, content = validate_skill_md(skill_dir)
92
+ if is_valid:
93
+ dest = skills_dir / skill_dir.name
94
+ if not dest.exists():
95
+ shutil.copytree(skill_dir, dest)
96
+ else:
97
+ click.echo(f"Warning: Invalid skill {skill_dir.name}: {errors}")
98
+
99
+ # Sync harness
100
+ repo_harness = repo_dir / "harness"
101
+ if repo_harness.exists():
102
+ for h_dir in repo_harness.iterdir():
103
+ if not h_dir.is_dir():
104
+ continue
105
+
106
+ dest = harness_dir / h_dir.name
107
+ if not dest.exists():
108
+ shutil.copytree(h_dir, dest)
109
+
110
+
111
+ def list_installed():
112
+ """List all installed agents, skills and harness."""
113
+ registry = load_registry()
114
+
115
+ agents = registry.get("agents", [])
116
+ skills = registry.get("skills", [])
117
+ harness = registry.get("harness", [])
118
+
119
+ if not agents and not skills and not harness:
120
+ click.echo("No agents/skills/harness installed.")
121
+ click.echo("Run: agents gou")
122
+ return
123
+
124
+ if agents:
125
+ click.echo("Agents:")
126
+ for agent in agents:
127
+ version = agent.get('version', 'unknown')
128
+ source = agent.get('source', 'unknown')
129
+ click.echo(f" - {agent['name']} v{version} ({source})")
130
+ click.echo()
131
+
132
+ if skills:
133
+ click.echo("Skills:")
134
+ for skill in skills:
135
+ source = skill.get('source', 'unknown')
136
+ click.echo(f" - {skill['name']} ({source})")
137
+ click.echo()
138
+
139
+ if harness:
140
+ click.echo("Harness:")
141
+ for h in harness:
142
+ source = h.get('source', 'unknown')
143
+ click.echo(f" - {h['name']} ({source})")
144
+
145
+
146
+ def find_agents():
147
+ """Find all agents in repos."""
148
+ agents = []
149
+ if not REPOS_DIR.exists():
150
+ return agents
151
+
152
+ for repo_dir in REPOS_DIR.iterdir():
153
+ if not repo_dir.is_dir():
154
+ continue
155
+
156
+ agents_dir = repo_dir / "agents"
157
+ if not agents_dir.exists():
158
+ continue
159
+
160
+ for agent_dir in agents_dir.iterdir():
161
+ if not agent_dir.is_dir():
162
+ continue
163
+
164
+ config_file = agent_dir / "agent.yaml"
165
+ if config_file.exists():
166
+ with open(config_file) as f:
167
+ config = yaml.safe_load(f) or {}
168
+
169
+ agents.append({
170
+ "name": config.get("name", agent_dir.name),
171
+ "version": config.get("version", "unknown"),
172
+ "source": repo_dir.name,
173
+ })
174
+
175
+ return agents
176
+
177
+
178
+ def find_skills():
179
+ """Find all skills in repos."""
180
+ skills = []
181
+ if not REPOS_DIR.exists():
182
+ return skills
183
+
184
+ for repo_dir in REPOS_DIR.iterdir():
185
+ if not repo_dir.is_dir():
186
+ continue
187
+
188
+ skills_dir = repo_dir / "skills"
189
+ if not skills_dir.exists():
190
+ continue
191
+
192
+ for skill_dir in skills_dir.iterdir():
193
+ if not skill_dir.is_dir():
194
+ continue
195
+
196
+ skill_file = skill_dir / "SKILL.md"
197
+ if skill_file.exists():
198
+ skills.append({
199
+ "name": skill_dir.name,
200
+ "source": repo_dir.name,
201
+ })
202
+
203
+ return skills
204
+
205
+
206
+ def find_harness():
207
+ """Find all harness configurations in repos."""
208
+ harness = []
209
+ if not REPOS_DIR.exists():
210
+ return harness
211
+
212
+ for repo_dir in REPOS_DIR.iterdir():
213
+ if not repo_dir.is_dir():
214
+ continue
215
+
216
+ harness_dir = repo_dir / "harness"
217
+ if not harness_dir.exists():
218
+ continue
219
+
220
+ for h_dir in harness_dir.iterdir():
221
+ if not h_dir.is_dir():
222
+ continue
223
+
224
+ harness.append({
225
+ "name": h_dir.name,
226
+ "source": repo_dir.name,
227
+ })
228
+
229
+ return harness
@@ -0,0 +1,86 @@
1
+ """Validation for agent/skill/harness definitions."""
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+
8
+ REQUIRED_AGENT_FIELDS = ["name"]
9
+ VALID_TOOL_TYPES = ["read", "write", "edit", "bash", "grep", "glob"]
10
+
11
+
12
+ def validate_agent_yaml(agent_dir):
13
+ """Validate agent.yaml in a directory.
14
+
15
+ Returns (is_valid, errors, config).
16
+ """
17
+ config_file = agent_dir / "agent.yaml"
18
+
19
+ if not config_file.exists():
20
+ return False, ["agent.yaml not found"], None
21
+
22
+ try:
23
+ with open(config_file) as f:
24
+ config = yaml.safe_load(f)
25
+ except yaml.YAMLError as e:
26
+ return False, [f"Invalid YAML: {e}"], None
27
+
28
+ if config is None:
29
+ return False, ["agent.yaml is empty"], None
30
+
31
+ errors = []
32
+
33
+ # Check required fields
34
+ for field in REQUIRED_AGENT_FIELDS:
35
+ if field not in config:
36
+ errors.append(f"Missing required field: {field}")
37
+
38
+ # Validate tools if present
39
+ if "tools" in config:
40
+ tools = config["tools"]
41
+ if not isinstance(tools, list):
42
+ errors.append("'tools' must be a list")
43
+ else:
44
+ for tool in tools:
45
+ if tool not in VALID_TOOL_TYPES:
46
+ errors.append(f"Unknown tool type: {tool}")
47
+
48
+ # Validate name format
49
+ if "name" in config:
50
+ name = config["name"]
51
+ if not isinstance(name, str) or not name.strip():
52
+ errors.append("'name' must be a non-empty string")
53
+
54
+ return len(errors) == 0, errors, config
55
+
56
+
57
+ def validate_skill_md(skill_dir):
58
+ """Validate SKILL.md in a directory.
59
+
60
+ Returns (is_valid, errors, content).
61
+ """
62
+ skill_file = skill_dir / "SKILL.md"
63
+
64
+ if not skill_file.exists():
65
+ return False, ["SKILL.md not found"], None
66
+
67
+ try:
68
+ content = skill_file.read_text()
69
+ except Exception as e:
70
+ return False, [f"Cannot read SKILL.md: {e}"], None
71
+
72
+ if not content.strip():
73
+ return False, ["SKILL.md is empty"], None
74
+
75
+ # Check for frontmatter
76
+ if content.startswith("---"):
77
+ try:
78
+ parts = content.split("---", 2)
79
+ if len(parts) >= 3:
80
+ frontmatter = yaml.safe_load(parts[1])
81
+ if frontmatter and "name" not in frontmatter:
82
+ return False, ["Frontmatter missing 'name' field"], content
83
+ except yaml.YAMLError:
84
+ return False, ["Invalid YAML in frontmatter"], content
85
+
86
+ return True, [], content
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.5
2
+ Name: agents-tool
3
+ Version: 0.1.0
4
+ Summary: CLI tool for managing AI agents, skills and harness
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: click>=8.1.0
9
+ Requires-Dist: gitpython>=3.1.0
10
+ Requires-Dist: pyyaml>=6.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Agents Tool
14
+
15
+ CLI tool for managing AI agents, skills and harness.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install agents-tool
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Initialize configuration
27
+ agents init
28
+
29
+ # Edit config to add your repos
30
+ vim ~/.config/agents/config.yaml
31
+
32
+ # Import/update all agents
33
+ agents gou
34
+
35
+ # List installed agents/skills/harness
36
+ agents list
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ Edit `~/.config/agents/config.yaml`:
42
+
43
+ ```yaml
44
+ repos:
45
+ - url: git@github.com:your-username/your-agents.git
46
+ user: your-username
47
+ branch: main
48
+ ```
49
+
50
+ ## Agent Repo Structure
51
+
52
+ ```
53
+ your-agents/
54
+ ├── agents/
55
+ │ └── reviewer/
56
+ │ ├── agent.yaml
57
+ │ └── prompt.md
58
+ ├── skills/
59
+ │ └── code-review/
60
+ │ └── SKILL.md
61
+ └── harness/
62
+ └── opencode/
63
+ └── config.json
64
+ ```
@@ -0,0 +1,11 @@
1
+ agents_tool/__init__.py,sha256=a6BznFb0FAS2EI_TXOqZLiuRdBRtdO1Jc2HdmFDHiQE,91
2
+ agents_tool/cli.py,sha256=x8bTS-QhT5tAMKjL8ptHH8LF8bGc7rzPfdps9gpRGl0,641
3
+ agents_tool/config.py,sha256=M0rTii7TusHlrK7Glx5i_IhCnA0Kt-cQ5VwWa99DsJU,1507
4
+ agents_tool/git.py,sha256=mo7RBHaGes4BjrnY93FlQuHHj26JI2vNvdBYOFuxZzU,2532
5
+ agents_tool/registry.py,sha256=mqAUxqDCuVjtXw9O4gNkLCZkKWe2tTSJa6_J_sHOjyk,6311
6
+ agents_tool/validator.py,sha256=x48PHYrtQgmU3zD0OenNU9uWHuk9uTwW044gpJr7zdw,2416
7
+ agents_tool-0.1.0.dist-info/METADATA,sha256=QMxjtVwidmVL4NBaRFxEJienW7T0AsRMulrqYv7yePU,1127
8
+ agents_tool-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ agents_tool-0.1.0.dist-info/entry_points.txt,sha256=pGdW_2B-utlHR6EtcBglUVGHd_QHnrMjz6DkR2tJ3UQ,48
10
+ agents_tool-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
11
+ agents_tool-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agents = agents_tool.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.