aes-cli 0.2.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.
- aes/__init__.py +5 -0
- aes/__main__.py +37 -0
- aes/analyzer.py +487 -0
- aes/commands/__init__.py +0 -0
- aes/commands/init.py +727 -0
- aes/commands/inspect.py +204 -0
- aes/commands/install.py +379 -0
- aes/commands/publish.py +432 -0
- aes/commands/search.py +65 -0
- aes/commands/status.py +153 -0
- aes/commands/sync.py +413 -0
- aes/commands/validate.py +77 -0
- aes/config.py +43 -0
- aes/domains.py +1382 -0
- aes/frameworks.py +522 -0
- aes/mcp_server.py +213 -0
- aes/registry.py +294 -0
- aes/scaffold/agent.yaml.jinja +135 -0
- aes/scaffold/agentignore.jinja +61 -0
- aes/scaffold/instructions.md.jinja +311 -0
- aes/scaffold/local.example.yaml.jinja +35 -0
- aes/scaffold/local.yaml.jinja +29 -0
- aes/scaffold/operations.md.jinja +33 -0
- aes/scaffold/orchestrator.md.jinja +95 -0
- aes/scaffold/permissions.yaml.jinja +151 -0
- aes/scaffold/setup.md.jinja +244 -0
- aes/scaffold/skill.md.jinja +27 -0
- aes/scaffold/skill.yaml.jinja +175 -0
- aes/scaffold/workflow.yaml.jinja +44 -0
- aes/scaffold/workflow_command.md.jinja +48 -0
- aes/schemas/agent.schema.json +188 -0
- aes/schemas/permissions.schema.json +100 -0
- aes/schemas/registry.schema.json +72 -0
- aes/schemas/skill.schema.json +209 -0
- aes/schemas/workflow.schema.json +92 -0
- aes/targets/__init__.py +29 -0
- aes/targets/_base.py +77 -0
- aes/targets/_composer.py +338 -0
- aes/targets/claude.py +153 -0
- aes/targets/copilot.py +48 -0
- aes/targets/cursor.py +46 -0
- aes/targets/windsurf.py +46 -0
- aes/validator.py +394 -0
- aes_cli-0.2.0.dist-info/METADATA +110 -0
- aes_cli-0.2.0.dist-info/RECORD +48 -0
- aes_cli-0.2.0.dist-info/WHEEL +5 -0
- aes_cli-0.2.0.dist-info/entry_points.txt +3 -0
- aes_cli-0.2.0.dist-info/top_level.txt +1 -0
aes/mcp_server.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""AES MCP Server — Model Context Protocol server for the AES registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import FastMCP
|
|
13
|
+
|
|
14
|
+
from aes.config import AGENT_DIR
|
|
15
|
+
from aes.registry import fetch_index, search_packages
|
|
16
|
+
from aes.validator import validate_agent_dir
|
|
17
|
+
|
|
18
|
+
# Logging must go to stderr — stdout is the JSON-RPC channel
|
|
19
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
|
|
20
|
+
logger = logging.getLogger("aes-mcp")
|
|
21
|
+
|
|
22
|
+
mcp = FastMCP("aes-registry")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Tools
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@mcp.tool()
|
|
31
|
+
def aes_search(
|
|
32
|
+
query: str = "",
|
|
33
|
+
tag: str = "",
|
|
34
|
+
domain: str = "",
|
|
35
|
+
pkg_type: str = "",
|
|
36
|
+
) -> str:
|
|
37
|
+
"""Search the AES package registry for skills and templates.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
query: Keyword to search package names and descriptions.
|
|
41
|
+
tag: Filter by tag (e.g. "ml", "devops").
|
|
42
|
+
domain: Filter by domain (convention: domain as tag).
|
|
43
|
+
pkg_type: Filter by type: "skill" or "template".
|
|
44
|
+
|
|
45
|
+
Returns a JSON array of matching packages.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
results = search_packages(
|
|
49
|
+
query=query or "",
|
|
50
|
+
tag=tag or None,
|
|
51
|
+
domain=domain or None,
|
|
52
|
+
pkg_type=pkg_type or None,
|
|
53
|
+
)
|
|
54
|
+
return json.dumps(results, indent=2)
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
return json.dumps({"error": f"Registry search failed: {exc}"})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@mcp.tool()
|
|
60
|
+
def aes_inspect(name: str) -> str:
|
|
61
|
+
"""Show details about a specific package in the AES registry.
|
|
62
|
+
|
|
63
|
+
Returns all versions, description, tags, and metadata for the package.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
name: Package name to inspect.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
index = fetch_index()
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
return json.dumps({"error": f"Failed to fetch registry index: {exc}"})
|
|
72
|
+
|
|
73
|
+
packages = index.get("packages", {})
|
|
74
|
+
if name not in packages:
|
|
75
|
+
return json.dumps({"error": f"Package '{name}' not found in registry."})
|
|
76
|
+
|
|
77
|
+
pkg = packages[name]
|
|
78
|
+
return json.dumps(
|
|
79
|
+
{
|
|
80
|
+
"name": name,
|
|
81
|
+
"type": pkg.get("type", "skill"),
|
|
82
|
+
"description": pkg.get("description", ""),
|
|
83
|
+
"tags": pkg.get("tags", []),
|
|
84
|
+
"latest": pkg.get("latest", ""),
|
|
85
|
+
"versions": pkg.get("versions", {}),
|
|
86
|
+
},
|
|
87
|
+
indent=2,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
def aes_install(
|
|
93
|
+
source: str,
|
|
94
|
+
project_path: str = ".",
|
|
95
|
+
force: bool = False,
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Install a skill or template from the AES registry into the project.
|
|
98
|
+
|
|
99
|
+
Installs into the .agent/skills/vendor/ directory and registers in agent.yaml.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
source: Package source — registry ref (e.g. "aes-hub/deploy@^1.0"),
|
|
103
|
+
local path, or tarball path.
|
|
104
|
+
project_path: Project root directory (defaults to current directory).
|
|
105
|
+
force: Overwrite existing vendor skills if True.
|
|
106
|
+
"""
|
|
107
|
+
import click
|
|
108
|
+
|
|
109
|
+
from aes.commands.install import (
|
|
110
|
+
_detect_source_type,
|
|
111
|
+
_install_local,
|
|
112
|
+
_install_registry,
|
|
113
|
+
_install_tarball,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
project_root = Path(project_path).resolve()
|
|
117
|
+
agent_dir = project_root / AGENT_DIR
|
|
118
|
+
|
|
119
|
+
if not agent_dir.exists():
|
|
120
|
+
return json.dumps(
|
|
121
|
+
{"error": f"No {AGENT_DIR}/ directory found at {project_root}"}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
source_type = _detect_source_type(source)
|
|
125
|
+
|
|
126
|
+
if source_type == "git":
|
|
127
|
+
return json.dumps({"error": "Git sources are not yet supported."})
|
|
128
|
+
if source_type == "unknown":
|
|
129
|
+
return json.dumps(
|
|
130
|
+
{"error": f"Cannot determine source type for '{source}'."}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Redirect stdout to stderr so rich.console.print doesn't corrupt
|
|
135
|
+
# the stdio JSON-RPC channel.
|
|
136
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
137
|
+
if source_type == "registry":
|
|
138
|
+
skill_id = _install_registry(source, project_root, force)
|
|
139
|
+
elif source_type == "tarball":
|
|
140
|
+
skill_id = _install_tarball(
|
|
141
|
+
Path(source).resolve(), project_root, force
|
|
142
|
+
)
|
|
143
|
+
elif source_type == "local":
|
|
144
|
+
skill_id = _install_local(source, project_root, force)
|
|
145
|
+
else:
|
|
146
|
+
return json.dumps({"error": f"Unsupported source type: {source_type}"})
|
|
147
|
+
|
|
148
|
+
return json.dumps(
|
|
149
|
+
{
|
|
150
|
+
"installed": skill_id,
|
|
151
|
+
"vendor_path": f".agent/skills/vendor/{skill_id}",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
except click.ClickException as exc:
|
|
155
|
+
return json.dumps({"error": exc.format_message()})
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
return json.dumps({"error": str(exc)})
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
def aes_validate(project_path: str = ".") -> str:
|
|
162
|
+
"""Validate the .agent/ directory against AES schemas.
|
|
163
|
+
|
|
164
|
+
Reports pass/fail for each file and any validation errors.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
project_path: Project root directory (defaults to current directory).
|
|
168
|
+
"""
|
|
169
|
+
project_root = Path(project_path).resolve()
|
|
170
|
+
agent_dir = project_root / AGENT_DIR
|
|
171
|
+
|
|
172
|
+
if not agent_dir.exists():
|
|
173
|
+
return json.dumps(
|
|
174
|
+
{"error": f"No {AGENT_DIR}/ directory found at {project_root}"}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
results = validate_agent_dir(agent_dir)
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
return json.dumps({"error": f"Validation failed: {exc}"})
|
|
181
|
+
|
|
182
|
+
passed = sum(1 for r in results if r.valid)
|
|
183
|
+
failed = sum(1 for r in results if not r.valid)
|
|
184
|
+
|
|
185
|
+
return json.dumps(
|
|
186
|
+
{
|
|
187
|
+
"summary": {"passed": passed, "failed": failed, "total": len(results)},
|
|
188
|
+
"results": [
|
|
189
|
+
{
|
|
190
|
+
"file": str(r.file_path),
|
|
191
|
+
"schema": r.schema_type,
|
|
192
|
+
"valid": r.valid,
|
|
193
|
+
"errors": r.errors,
|
|
194
|
+
}
|
|
195
|
+
for r in results
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
indent=2,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Entry point
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main() -> None:
|
|
208
|
+
"""Run the AES MCP server over stdio."""
|
|
209
|
+
mcp.run(transport="stdio")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
aes/registry.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Registry client — fetch index, resolve versions, download packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import urllib.request
|
|
10
|
+
import urllib.error
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
# Default registry URL (R2/S3 public bucket)
|
|
15
|
+
REGISTRY_URL = os.environ.get("AES_REGISTRY_URL", "https://registry.aes-official.com")
|
|
16
|
+
INDEX_PATH = "index.json"
|
|
17
|
+
PACKAGES_PATH = "packages"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_version(v: str) -> Tuple[int, int, int]:
|
|
21
|
+
"""Parse a semver string into (major, minor, patch)."""
|
|
22
|
+
match = re.match(r"^(\d+)\.(\d+)\.(\d+)", v)
|
|
23
|
+
if not match:
|
|
24
|
+
raise ValueError(f"Invalid semver: {v}")
|
|
25
|
+
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _version_matches(version: str, spec: str) -> bool:
|
|
29
|
+
"""Check if *version* satisfies *spec*.
|
|
30
|
+
|
|
31
|
+
Supports: exact "1.2.3", caret "^1.2.0", tilde "~1.2.0",
|
|
32
|
+
minimum ">=1.0.0", wildcard "*".
|
|
33
|
+
"""
|
|
34
|
+
if spec == "*":
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
ver = _parse_version(version)
|
|
38
|
+
|
|
39
|
+
if spec.startswith("^"):
|
|
40
|
+
base = _parse_version(spec[1:])
|
|
41
|
+
# >=base, <next_major (or <next_minor if major==0)
|
|
42
|
+
if base[0] == 0:
|
|
43
|
+
ceiling = (0, base[1] + 1, 0)
|
|
44
|
+
else:
|
|
45
|
+
ceiling = (base[0] + 1, 0, 0)
|
|
46
|
+
return base <= ver < ceiling
|
|
47
|
+
|
|
48
|
+
if spec.startswith("~"):
|
|
49
|
+
base = _parse_version(spec[1:])
|
|
50
|
+
ceiling = (base[0], base[1] + 1, 0)
|
|
51
|
+
return base <= ver < ceiling
|
|
52
|
+
|
|
53
|
+
if spec.startswith(">="):
|
|
54
|
+
base = _parse_version(spec[2:])
|
|
55
|
+
return ver >= base
|
|
56
|
+
|
|
57
|
+
if spec.startswith(">"):
|
|
58
|
+
base = _parse_version(spec[1:])
|
|
59
|
+
return ver > base
|
|
60
|
+
|
|
61
|
+
if spec.startswith("<="):
|
|
62
|
+
base = _parse_version(spec[2:])
|
|
63
|
+
return ver <= base
|
|
64
|
+
|
|
65
|
+
if spec.startswith("<"):
|
|
66
|
+
base = _parse_version(spec[1:])
|
|
67
|
+
return ver < base
|
|
68
|
+
|
|
69
|
+
# Exact match
|
|
70
|
+
return ver == _parse_version(spec)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_version(spec: str, available: List[str]) -> Optional[str]:
|
|
74
|
+
"""Find the best (highest) version matching *spec*."""
|
|
75
|
+
matches = [v for v in available if _version_matches(v, spec)]
|
|
76
|
+
if not matches:
|
|
77
|
+
return None
|
|
78
|
+
matches.sort(key=_parse_version)
|
|
79
|
+
return matches[-1]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_registry_source(source: str) -> Tuple[str, str]:
|
|
86
|
+
"""Parse ``aes-hub/deploy@^1.2.0`` into (name, version_spec).
|
|
87
|
+
|
|
88
|
+
Returns (name, "*") if no version constraint given.
|
|
89
|
+
Raises ValueError if name is invalid.
|
|
90
|
+
"""
|
|
91
|
+
# Strip prefix
|
|
92
|
+
name_ver = source
|
|
93
|
+
if "/" in name_ver:
|
|
94
|
+
name_ver = name_ver.split("/", 1)[1]
|
|
95
|
+
|
|
96
|
+
if "@" in name_ver:
|
|
97
|
+
name, version_spec = name_ver.split("@", 1)
|
|
98
|
+
else:
|
|
99
|
+
name = name_ver
|
|
100
|
+
version_spec = "*"
|
|
101
|
+
|
|
102
|
+
if not _NAME_RE.match(name):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Invalid package name {name!r}: must be 1-64 lowercase chars "
|
|
105
|
+
"starting with a letter (a-z, 0-9, _, -)"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return name, version_spec
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def fetch_index(registry_url: Optional[str] = None) -> dict:
|
|
112
|
+
"""Fetch and parse the registry ``index.json``."""
|
|
113
|
+
base = registry_url or os.environ.get("AES_REGISTRY_URL", REGISTRY_URL)
|
|
114
|
+
url = f"{base.rstrip('/')}/{INDEX_PATH}"
|
|
115
|
+
|
|
116
|
+
req = urllib.request.Request(url)
|
|
117
|
+
token = os.environ.get("AES_REGISTRY_KEY")
|
|
118
|
+
if token:
|
|
119
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
120
|
+
|
|
121
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
122
|
+
return json.loads(resp.read().decode())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def download_package(
|
|
126
|
+
name: str,
|
|
127
|
+
version: str,
|
|
128
|
+
sha256_expected: str,
|
|
129
|
+
dest: Path,
|
|
130
|
+
registry_url: Optional[str] = None,
|
|
131
|
+
) -> Path:
|
|
132
|
+
"""Download a tarball from the registry and verify its sha256.
|
|
133
|
+
|
|
134
|
+
Returns the local path to the downloaded file.
|
|
135
|
+
"""
|
|
136
|
+
base = registry_url or os.environ.get("AES_REGISTRY_URL", REGISTRY_URL)
|
|
137
|
+
url = f"{base.rstrip('/')}/{PACKAGES_PATH}/{name}/{version}.tar.gz"
|
|
138
|
+
|
|
139
|
+
tarball_path = dest / f"{name}-{version}.tar.gz"
|
|
140
|
+
|
|
141
|
+
req = urllib.request.Request(url)
|
|
142
|
+
token = os.environ.get("AES_REGISTRY_KEY")
|
|
143
|
+
if token:
|
|
144
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
145
|
+
|
|
146
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
147
|
+
data = resp.read()
|
|
148
|
+
|
|
149
|
+
actual_sha = hashlib.sha256(data).hexdigest()
|
|
150
|
+
if actual_sha != sha256_expected:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"SHA256 mismatch for {name}@{version}: "
|
|
153
|
+
f"expected {sha256_expected[:16]}..., got {actual_sha[:16]}..."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
tarball_path.write_bytes(data)
|
|
157
|
+
return tarball_path
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def upload_package(
|
|
161
|
+
tarball_path: Path,
|
|
162
|
+
name: str,
|
|
163
|
+
version: str,
|
|
164
|
+
description: str,
|
|
165
|
+
tags: Optional[List[str]] = None,
|
|
166
|
+
registry_url: Optional[str] = None,
|
|
167
|
+
pkg_type: str = "skill",
|
|
168
|
+
visibility: str = "public",
|
|
169
|
+
) -> dict:
|
|
170
|
+
"""Upload a tarball and update the registry index.
|
|
171
|
+
|
|
172
|
+
Returns the updated index entry for this package.
|
|
173
|
+
"""
|
|
174
|
+
base = registry_url or os.environ.get("AES_REGISTRY_URL", REGISTRY_URL)
|
|
175
|
+
token = os.environ.get("AES_REGISTRY_KEY")
|
|
176
|
+
if not token:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
"AES_REGISTRY_KEY environment variable is required for publishing. "
|
|
179
|
+
"Set it to your registry API token."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
tarball_data = tarball_path.read_bytes()
|
|
183
|
+
sha = hashlib.sha256(tarball_data).hexdigest()
|
|
184
|
+
|
|
185
|
+
# Upload tarball
|
|
186
|
+
upload_url = f"{base.rstrip('/')}/{PACKAGES_PATH}/{name}/{version}.tar.gz"
|
|
187
|
+
req = urllib.request.Request(upload_url, data=tarball_data, method="PUT")
|
|
188
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
189
|
+
req.add_header("Content-Type", "application/gzip")
|
|
190
|
+
try:
|
|
191
|
+
urllib.request.urlopen(req, timeout=120)
|
|
192
|
+
except urllib.error.HTTPError as exc:
|
|
193
|
+
if exc.code == 409:
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
f"Version {version} of '{name}' already exists in the registry. "
|
|
196
|
+
"Bump the version in your skill manifest and try again."
|
|
197
|
+
) from None
|
|
198
|
+
raise
|
|
199
|
+
|
|
200
|
+
# Fetch current index
|
|
201
|
+
try:
|
|
202
|
+
index = fetch_index(registry_url)
|
|
203
|
+
except (urllib.error.URLError, urllib.error.HTTPError):
|
|
204
|
+
index = {"packages": {}}
|
|
205
|
+
|
|
206
|
+
# Update index
|
|
207
|
+
packages = index.setdefault("packages", {})
|
|
208
|
+
pkg = packages.setdefault(name, {
|
|
209
|
+
"description": description,
|
|
210
|
+
"latest": version,
|
|
211
|
+
"versions": {},
|
|
212
|
+
})
|
|
213
|
+
pkg["description"] = description
|
|
214
|
+
pkg["type"] = pkg_type
|
|
215
|
+
pkg["visibility"] = visibility
|
|
216
|
+
if tags:
|
|
217
|
+
pkg["tags"] = tags
|
|
218
|
+
|
|
219
|
+
from datetime import datetime, timezone
|
|
220
|
+
pkg["versions"][version] = {
|
|
221
|
+
"url": f"{PACKAGES_PATH}/{name}/{version}.tar.gz",
|
|
222
|
+
"sha256": sha,
|
|
223
|
+
"published_at": datetime.now(timezone.utc).isoformat(),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Update "latest" if this version is higher
|
|
227
|
+
try:
|
|
228
|
+
if _parse_version(version) >= _parse_version(pkg.get("latest", "0.0.0")):
|
|
229
|
+
pkg["latest"] = version
|
|
230
|
+
except ValueError:
|
|
231
|
+
pkg["latest"] = version
|
|
232
|
+
|
|
233
|
+
# Upload updated index
|
|
234
|
+
index_data = json.dumps(index, indent=2).encode()
|
|
235
|
+
index_url = f"{base.rstrip('/')}/{INDEX_PATH}"
|
|
236
|
+
req = urllib.request.Request(index_url, data=index_data, method="PUT")
|
|
237
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
238
|
+
req.add_header("Content-Type", "application/json")
|
|
239
|
+
urllib.request.urlopen(req, timeout=30)
|
|
240
|
+
|
|
241
|
+
return pkg
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def search_packages(
|
|
245
|
+
query: str = "",
|
|
246
|
+
tag: Optional[str] = None,
|
|
247
|
+
domain: Optional[str] = None,
|
|
248
|
+
index: Optional[dict] = None,
|
|
249
|
+
registry_url: Optional[str] = None,
|
|
250
|
+
pkg_type: Optional[str] = None,
|
|
251
|
+
) -> List[Dict[str, object]]:
|
|
252
|
+
"""Search the registry index for matching packages.
|
|
253
|
+
|
|
254
|
+
Returns list of dicts with: name, description, latest, tags, type.
|
|
255
|
+
"""
|
|
256
|
+
if index is None:
|
|
257
|
+
index = fetch_index(registry_url)
|
|
258
|
+
|
|
259
|
+
results = []
|
|
260
|
+
for name, pkg in index.get("packages", {}).items():
|
|
261
|
+
# Type filter (packages without type default to "skill")
|
|
262
|
+
if pkg_type:
|
|
263
|
+
if pkg.get("type", "skill") != pkg_type:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Keyword filter
|
|
267
|
+
if query:
|
|
268
|
+
q = query.lower()
|
|
269
|
+
haystack = f"{name} {pkg.get('description', '')}".lower()
|
|
270
|
+
if q not in haystack:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Tag filter
|
|
274
|
+
if tag:
|
|
275
|
+
pkg_tags = pkg.get("tags", [])
|
|
276
|
+
if tag.lower() not in [t.lower() for t in pkg_tags]:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Domain filter (convention: domain is a tag)
|
|
280
|
+
if domain:
|
|
281
|
+
pkg_tags = pkg.get("tags", [])
|
|
282
|
+
if domain.lower() not in [t.lower() for t in pkg_tags]:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
results.append({
|
|
286
|
+
"name": name,
|
|
287
|
+
"description": pkg.get("description", ""),
|
|
288
|
+
"latest": pkg.get("latest", "?"),
|
|
289
|
+
"tags": pkg.get("tags", []),
|
|
290
|
+
"type": pkg.get("type", "skill"),
|
|
291
|
+
"versions": list(pkg.get("versions", {}).keys()),
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return results
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{% if domain_config %}
|
|
2
|
+
# .agent/agent.yaml — AES Manifest
|
|
3
|
+
aes: "1.0"
|
|
4
|
+
|
|
5
|
+
name: "{{ name }}"
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
description: "{{ domain_config.instructions_description or 'TODO: describe what this agentic system does' }}"
|
|
8
|
+
domain: "{{ domain }}"
|
|
9
|
+
license: "MIT"
|
|
10
|
+
|
|
11
|
+
runtime:
|
|
12
|
+
language: "{{ language }}"
|
|
13
|
+
|
|
14
|
+
agent:
|
|
15
|
+
instructions: "instructions.md"
|
|
16
|
+
permissions: "permissions.yaml"
|
|
17
|
+
{% if has_skills %}
|
|
18
|
+
orchestrator: "skills/ORCHESTRATOR.md"
|
|
19
|
+
{% if domain_config.skills %}
|
|
20
|
+
|
|
21
|
+
skills:
|
|
22
|
+
{% for skill in domain_config.skills %}
|
|
23
|
+
- id: "{{ skill.id }}"
|
|
24
|
+
manifest: "skills/{{ skill.id }}.skill.yaml"
|
|
25
|
+
runbook: "skills/{{ skill.id }}.md"
|
|
26
|
+
{% endfor %}
|
|
27
|
+
{% else %}
|
|
28
|
+
|
|
29
|
+
skills: []
|
|
30
|
+
{% endif %}
|
|
31
|
+
{% endif %}
|
|
32
|
+
{% if has_workflows and domain_config.workflow %}
|
|
33
|
+
|
|
34
|
+
workflows:
|
|
35
|
+
- id: "{{ domain_config.workflow.id }}"
|
|
36
|
+
path: "workflows/{{ domain_config.workflow.id }}.yaml"
|
|
37
|
+
{% endif %}
|
|
38
|
+
|
|
39
|
+
commands:
|
|
40
|
+
- id: "setup"
|
|
41
|
+
path: "commands/setup.md"
|
|
42
|
+
trigger: "/setup"
|
|
43
|
+
description: "Review and customize .agent/ files for your project"
|
|
44
|
+
{% if domain_config.workflow_commands %}
|
|
45
|
+
{% for cmd in domain_config.workflow_commands %}
|
|
46
|
+
- id: "{{ cmd.id }}"
|
|
47
|
+
path: "commands/{{ cmd.id }}.md"
|
|
48
|
+
trigger: "{{ cmd.trigger }}"
|
|
49
|
+
description: "{{ cmd.description }}"
|
|
50
|
+
{% endfor %}
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
environment:
|
|
54
|
+
{% if domain_config.env_required %}
|
|
55
|
+
required:
|
|
56
|
+
{% for env in domain_config.env_required %}
|
|
57
|
+
- name: "{{ env.name }}"
|
|
58
|
+
description: "{{ env.description }}"
|
|
59
|
+
{% endfor %}
|
|
60
|
+
{% else %}
|
|
61
|
+
required: []
|
|
62
|
+
{% endif %}
|
|
63
|
+
{% if domain_config.env_optional %}
|
|
64
|
+
optional:
|
|
65
|
+
{% for env in domain_config.env_optional %}
|
|
66
|
+
- name: "{{ env.name }}"
|
|
67
|
+
default: "{{ env.default }}"
|
|
68
|
+
description: "{{ env.description }}"
|
|
69
|
+
{% endfor %}
|
|
70
|
+
{% else %}
|
|
71
|
+
optional: []
|
|
72
|
+
{% endif %}
|
|
73
|
+
{% else %}
|
|
74
|
+
# .agent/agent.yaml — AES Manifest
|
|
75
|
+
aes: "1.0"
|
|
76
|
+
|
|
77
|
+
name: "{{ name }}"
|
|
78
|
+
version: "0.1.0"
|
|
79
|
+
# <!-- AGENT: Describe what this agentic system does in one sentence.
|
|
80
|
+
# If code exists: read README + entry point + package config.
|
|
81
|
+
# If greenfield: ask the user what they're building. -->
|
|
82
|
+
description: "TODO: describe what this agentic system does"
|
|
83
|
+
domain: "{{ domain }}"
|
|
84
|
+
license: "MIT"
|
|
85
|
+
|
|
86
|
+
runtime:
|
|
87
|
+
language: "{{ language }}"
|
|
88
|
+
|
|
89
|
+
agent:
|
|
90
|
+
instructions: "instructions.md"
|
|
91
|
+
permissions: "permissions.yaml"
|
|
92
|
+
{% if has_skills %}
|
|
93
|
+
orchestrator: "skills/ORCHESTRATOR.md"
|
|
94
|
+
|
|
95
|
+
skills: []
|
|
96
|
+
# - id: "example"
|
|
97
|
+
# manifest: "skills/example.skill.yaml"
|
|
98
|
+
# runbook: "skills/example.md"
|
|
99
|
+
{% endif %}
|
|
100
|
+
{% if has_registry %}
|
|
101
|
+
|
|
102
|
+
registries: []
|
|
103
|
+
# - id: "components"
|
|
104
|
+
# path: "registry/components.yaml"
|
|
105
|
+
# description: "Component definitions"
|
|
106
|
+
{% endif %}
|
|
107
|
+
{% if has_workflows %}
|
|
108
|
+
|
|
109
|
+
workflows: []
|
|
110
|
+
# - id: "pipeline"
|
|
111
|
+
# path: "workflows/pipeline.yaml"
|
|
112
|
+
{% endif %}
|
|
113
|
+
|
|
114
|
+
commands:
|
|
115
|
+
- id: "setup"
|
|
116
|
+
path: "commands/setup.md"
|
|
117
|
+
trigger: "/setup"
|
|
118
|
+
description: "Populate .agent/ files by analyzing the codebase"
|
|
119
|
+
# - id: "run"
|
|
120
|
+
# path: "commands/run.md"
|
|
121
|
+
# trigger: "/run"
|
|
122
|
+
# description: "Run the main workflow"
|
|
123
|
+
|
|
124
|
+
environment:
|
|
125
|
+
# <!-- AGENT: List required and optional environment variables.
|
|
126
|
+
# If code exists: scan for os.environ, process.env, .env, .env.example.
|
|
127
|
+
# If greenfield: ask "any API keys or config vars needed?" -->
|
|
128
|
+
required: []
|
|
129
|
+
# - name: "API_KEY"
|
|
130
|
+
# description: "Main API key"
|
|
131
|
+
optional: []
|
|
132
|
+
# - name: "TIMEOUT"
|
|
133
|
+
# default: "300"
|
|
134
|
+
# description: "Operation timeout"
|
|
135
|
+
{% endif %}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# .agentignore — Files agents should never read or modify
|
|
2
|
+
|
|
3
|
+
# Secrets
|
|
4
|
+
.env
|
|
5
|
+
.env.*
|
|
6
|
+
*.pem
|
|
7
|
+
*.key
|
|
8
|
+
*.p12
|
|
9
|
+
credentials.json
|
|
10
|
+
|
|
11
|
+
# Databases
|
|
12
|
+
*.sqlite3
|
|
13
|
+
*.db
|
|
14
|
+
|
|
15
|
+
# Binary artifacts
|
|
16
|
+
{% if language == "python" %}
|
|
17
|
+
*.joblib
|
|
18
|
+
*.pkl
|
|
19
|
+
*.h5
|
|
20
|
+
*.pt
|
|
21
|
+
{% elif language == "javascript" or language == "typescript" %}
|
|
22
|
+
*.wasm
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
# Large data
|
|
26
|
+
data/**/*.parquet
|
|
27
|
+
data/**/*.csv
|
|
28
|
+
|
|
29
|
+
# Build artifacts
|
|
30
|
+
{% if language == "python" %}
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
__pycache__/
|
|
35
|
+
*.pyc
|
|
36
|
+
.venv/
|
|
37
|
+
{% elif language == "javascript" or language == "typescript" %}
|
|
38
|
+
node_modules/
|
|
39
|
+
dist/
|
|
40
|
+
.next/
|
|
41
|
+
{% elif language == "go" %}
|
|
42
|
+
vendor/
|
|
43
|
+
{% elif language == "rust" %}
|
|
44
|
+
target/
|
|
45
|
+
{% elif language == "java" %}
|
|
46
|
+
target/
|
|
47
|
+
build/
|
|
48
|
+
*.class
|
|
49
|
+
*.jar
|
|
50
|
+
{% endif %}
|
|
51
|
+
|
|
52
|
+
# System
|
|
53
|
+
.git/
|
|
54
|
+
.DS_Store
|
|
55
|
+
Thumbs.db
|
|
56
|
+
|
|
57
|
+
# Agent session data
|
|
58
|
+
.agent/memory/sessions/
|
|
59
|
+
|
|
60
|
+
# Local config (not committed)
|
|
61
|
+
.agent/local.yaml
|