chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate cli-hub-skill/SKILL.md from registry.json, public_registry.json, and matrix_registry.json."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
def main():
|
||||
repo_root = Path(__file__).parent.parent.parent
|
||||
registry_path = repo_root / 'registry.json'
|
||||
public_registry_path = repo_root / 'public_registry.json'
|
||||
matrix_registry_path = repo_root / 'matrix_registry.json'
|
||||
output_path = repo_root / 'cli-hub-skill' / 'SKILL.md'
|
||||
|
||||
with open(registry_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
public_clis = []
|
||||
if public_registry_path.exists():
|
||||
with open(public_registry_path) as f:
|
||||
public_data = json.load(f)
|
||||
public_clis = public_data.get('clis', [])
|
||||
|
||||
matrices = []
|
||||
if matrix_registry_path.exists():
|
||||
with open(matrix_registry_path) as f:
|
||||
matrix_data = json.load(f)
|
||||
matrices = matrix_data.get('matrices', [])
|
||||
|
||||
total_count = len(data['clis']) + len(public_clis)
|
||||
|
||||
# Group harness CLIs by category
|
||||
by_category = defaultdict(list)
|
||||
for cli in data['clis']:
|
||||
by_category[cli['category']].append(cli)
|
||||
|
||||
# Group public CLIs by category
|
||||
public_by_category = defaultdict(list)
|
||||
for cli in public_clis:
|
||||
public_by_category[cli['category']].append(cli)
|
||||
|
||||
lines = [
|
||||
"---",
|
||||
"name: cli-anything-hub",
|
||||
"description: >-",
|
||||
f" Browse and install {total_count}+ CLI tools for GUI software and popular platforms.",
|
||||
" Covers image editing, 3D, video, audio, office, diagrams, AI, communication, devops, and more.",
|
||||
"---",
|
||||
"",
|
||||
"# CLI-Anything Hub",
|
||||
"",
|
||||
f"Agent-native CLI interfaces for {total_count} applications — {len(data['clis'])} harness CLIs (stateful, `--json`, REPL) plus {len(public_clis)} public/third-party CLIs (npm, uv, brew, and more).",
|
||||
"",
|
||||
"## Quick Install",
|
||||
"",
|
||||
"```bash",
|
||||
"# First, install the CLI Hub package manager",
|
||||
"pip install cli-anything-hub",
|
||||
"",
|
||||
"# Browse available CLIs",
|
||||
"cli-hub list",
|
||||
"",
|
||||
"# Install any CLI by name",
|
||||
"cli-hub install gimp",
|
||||
"cli-hub install blender",
|
||||
"cli-hub install generate-veo-video",
|
||||
"",
|
||||
"# Search by category or keyword",
|
||||
"cli-hub search image",
|
||||
"cli-hub search ai",
|
||||
"",
|
||||
"# Launch an installed CLI",
|
||||
"cli-hub launch <name> [args...]",
|
||||
"```",
|
||||
"",
|
||||
"## CLI Matrices",
|
||||
"",
|
||||
f"`cli-hub` also ships {len(matrices)} curated cross-tool matrices: install one name to pull in a whole workflow kit and read its dedicated SKILL.md.",
|
||||
"",
|
||||
"```bash",
|
||||
"# Browse curated matrices",
|
||||
"cli-hub matrix list",
|
||||
"",
|
||||
"# Inspect one matrix",
|
||||
"cli-hub matrix info video-creation",
|
||||
"",
|
||||
"# Check which providers are available locally",
|
||||
"cli-hub matrix preflight video-creation --json",
|
||||
"",
|
||||
"# Install the whole matrix",
|
||||
"cli-hub matrix install video-creation",
|
||||
"```",
|
||||
"",
|
||||
"## CLI-Anything Harness CLIs",
|
||||
"",
|
||||
f"Stateful, agent-native wrappers for {len(data['clis'])} GUI applications. All support `--json` output, REPL mode, and undo/redo.",
|
||||
""
|
||||
]
|
||||
|
||||
for category in sorted(by_category.keys()):
|
||||
clis = by_category[category]
|
||||
lines.append(f"### {category.title()}")
|
||||
lines.append("")
|
||||
lines.append("| Name | Description | Install |")
|
||||
lines.append("|------|-------------|---------|")
|
||||
|
||||
for cli in sorted(clis, key=lambda x: x['name']):
|
||||
name = cli['display_name']
|
||||
desc = cli['description']
|
||||
install = f"`cli-hub install {cli['name']}`"
|
||||
lines.append(f"| **{name}** | {desc} | {install} |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"## Public & Third-Party CLIs",
|
||||
"",
|
||||
f"Official and community CLIs for popular platforms, managed via npm, uv, brew, and other installers. {len(public_clis)} CLIs available.",
|
||||
""
|
||||
])
|
||||
|
||||
for category in sorted(public_by_category.keys()):
|
||||
clis = public_by_category[category]
|
||||
lines.append(f"### {category.title()}")
|
||||
lines.append("")
|
||||
lines.append("| Name | Description | Entry Point | Install | Skill |")
|
||||
lines.append("|------|-------------|-------------|---------|-------|")
|
||||
|
||||
for cli in sorted(clis, key=lambda x: x['name']):
|
||||
name = cli['display_name']
|
||||
desc = cli['description']
|
||||
entry = f"`{cli['entry_point']}`"
|
||||
install = f"`cli-hub install {cli['name']}`"
|
||||
skill = cli.get('skill_md') or '—'
|
||||
skill_cell = f"`{skill}`" if not str(skill).startswith("http") else skill
|
||||
lines.append(f"| **{name}** | {desc} | {entry} | {install} | {skill_cell} |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
if matrices:
|
||||
lines.extend([
|
||||
"## Curated Matrices",
|
||||
"",
|
||||
"Each matrix is a curated multi-CLI workflow pulled from the CLI Matrix. Installing a matrix installs all member CLIs and points you at a matrix-specific SKILL.md.",
|
||||
"",
|
||||
"| Matrix | Description | CLIs | Install | Skill |",
|
||||
"|--------|-------------|------|---------|-------|",
|
||||
])
|
||||
|
||||
for matrix in sorted(matrices, key=lambda x: x['name']):
|
||||
skill = matrix.get('skill_md') or '—'
|
||||
install = f"`cli-hub matrix install {matrix['name']}`"
|
||||
lines.append(
|
||||
f"| **{matrix['display_name']}** | {matrix['description']} | {len(matrix.get('clis', []))} | {install} | `{skill}` |"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"## How It Works",
|
||||
"",
|
||||
"`cli-hub` is a unified package manager for both harness CLIs and public CLIs:",
|
||||
"",
|
||||
"- **Harness CLIs**: installed via `pip` as `cli-anything-<name>` packages",
|
||||
"- **npm CLIs**: installed via `npm install -g`",
|
||||
"- **uv CLIs**: installed via `uv tool install`",
|
||||
"- **brew/script CLIs**: installed via the tool's native installer",
|
||||
"- **bundled CLIs**: detected from PATH (pre-installed with the host app)",
|
||||
"- **Matrices**: install a curated set of harness and public CLIs in one command",
|
||||
"",
|
||||
"## Harness CLI Usage Pattern",
|
||||
"",
|
||||
"All harness CLIs follow the same pattern:",
|
||||
"",
|
||||
"```bash",
|
||||
"# Interactive REPL",
|
||||
"cli-anything-<name>",
|
||||
"",
|
||||
"# One-shot command",
|
||||
"cli-anything-<name> <group> <command> [options]",
|
||||
"",
|
||||
"# JSON output for agents",
|
||||
"cli-anything-<name> --json <group> <command>",
|
||||
"```",
|
||||
"",
|
||||
"## For AI Agents",
|
||||
"",
|
||||
"1. Install the hub: `pip install cli-anything-hub`",
|
||||
"2. Install the CLI you need: `cli-hub install <name>`",
|
||||
"3. Run the CLI directly via its entry point, or use `cli-hub launch <name> [args...]`",
|
||||
"4. For harness CLIs: use `--json` flag for machine-readable output; check exit codes (0=success)",
|
||||
"5. Read each harness CLI's full SKILL.md at the repo path shown in registry.json",
|
||||
"",
|
||||
"## More Info",
|
||||
"",
|
||||
f"- Repository: {data['meta']['repo']}",
|
||||
"- Web Hub: https://clianything.cc",
|
||||
f"- Last Updated: {data['meta']['updated']}",
|
||||
])
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text('\n'.join(lines) + '\n')
|
||||
print(
|
||||
f"Generated meta-skill with {len(data['clis'])} harness CLIs + "
|
||||
f"{len(public_clis)} public CLIs + {len(matrices)} matrices at {output_path}"
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
const LABELS = {
|
||||
"new-cli": {
|
||||
color: "0E8A16",
|
||||
description: "Adds a new CLI or generated harness",
|
||||
},
|
||||
"existing-cli-fix": {
|
||||
color: "1D76DB",
|
||||
description: "Fixes or improves an existing CLI harness",
|
||||
},
|
||||
"cli-anything-skill": {
|
||||
color: "5319E7",
|
||||
description: "Changes CLI-Anything plugin or skill files",
|
||||
},
|
||||
"cli-anything-hub": {
|
||||
color: "FBCA04",
|
||||
description: "Changes CLI-Hub, registries, or hub docs",
|
||||
},
|
||||
"documentation": {
|
||||
color: "0075CA",
|
||||
description: "Documentation issue or improvement",
|
||||
},
|
||||
"github-actions": {
|
||||
color: "6F42C1",
|
||||
description: "Changes GitHub Actions or automation",
|
||||
},
|
||||
};
|
||||
|
||||
const SCRIPT_MANAGED_LABELS = ["new-cli", "existing-cli-fix", "documentation"];
|
||||
const REGISTRY_FILES = new Set([
|
||||
"registry.json",
|
||||
"public_registry.json",
|
||||
"matrix_registry.json",
|
||||
]);
|
||||
|
||||
function isHarnessFile(path) {
|
||||
return /^[^/]+\/agent-harness\//.test(path);
|
||||
}
|
||||
|
||||
function isNewHarnessManifest(file) {
|
||||
return (
|
||||
file.status === "added" &&
|
||||
/^[^/]+\/agent-harness\/(setup\.py|pyproject\.toml)$/.test(file.filename)
|
||||
);
|
||||
}
|
||||
|
||||
function isDocumentationFile(path) {
|
||||
if (/(^|\/)SKILL\.md$/i.test(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
/^README(?:_[A-Z]+)?\.md$/.test(path) ||
|
||||
/^(CONTRIBUTING|SECURITY)\.md$/.test(path) ||
|
||||
/^docs\//.test(path) ||
|
||||
/^[^/]+\/agent-harness\/.*\.md$/i.test(path) ||
|
||||
/^[^/]+\.md$/i.test(path)
|
||||
);
|
||||
}
|
||||
|
||||
function isHarnessImplementationFile(path) {
|
||||
return isHarnessFile(path) && !isDocumentationFile(path);
|
||||
}
|
||||
|
||||
function titleLooksLikeRegistryCli(title) {
|
||||
return /\b(add|introduce|new)\b/i.test(title) && /\b(cli|harness|registry)\b/i.test(title);
|
||||
}
|
||||
|
||||
function computeScriptLabels(files, title) {
|
||||
const paths = files.map((file) => file.filename);
|
||||
const labelsToApply = new Set();
|
||||
|
||||
const hasHarnessImplementationChange = paths.some(isHarnessImplementationFile);
|
||||
const hasNewHarness = files.some(isNewHarnessManifest);
|
||||
const registryOnly = paths.length > 0 && paths.every((path) => REGISTRY_FILES.has(path));
|
||||
const registryNewCli = registryOnly && titleLooksLikeRegistryCli(title || "");
|
||||
const documentationOnly = paths.length > 0 && paths.every(isDocumentationFile);
|
||||
|
||||
if (hasNewHarness || registryNewCli) {
|
||||
labelsToApply.add("new-cli");
|
||||
} else if (hasHarnessImplementationChange) {
|
||||
labelsToApply.add("existing-cli-fix");
|
||||
}
|
||||
|
||||
if (documentationOnly) {
|
||||
labelsToApply.add("documentation");
|
||||
}
|
||||
|
||||
return labelsToApply;
|
||||
}
|
||||
|
||||
async function ensureLabels(github, owner, repo, core) {
|
||||
const existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const existingNames = new Set(existing.map((label) => label.name));
|
||||
|
||||
for (const [name, definition] of Object.entries(LABELS)) {
|
||||
if (existingNames.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(`Creating missing label: ${name}`);
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name,
|
||||
color: definition.color,
|
||||
description: definition.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function syncScriptLabels(github, owner, repo, pullNumber, currentLabels, labelsToApply, core) {
|
||||
for (const label of labelsToApply) {
|
||||
if (currentLabels.has(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(`Adding label: ${label}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
labels: [label],
|
||||
});
|
||||
}
|
||||
|
||||
for (const label of SCRIPT_MANAGED_LABELS) {
|
||||
if (!currentLabels.has(label) || labelsToApply.has(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(`Removing label: ${label}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
name: label,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const pullRequest = context.payload.pull_request;
|
||||
if (!pullRequest) {
|
||||
core.info("No pull_request payload found; skipping PR labeling.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pullNumber = pullRequest.number;
|
||||
|
||||
await ensureLabels(github, owner, repo, core);
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const labelsToApply = computeScriptLabels(files, pullRequest.title);
|
||||
|
||||
const currentLabels = new Set((pullRequest.labels || []).map((label) => label.name));
|
||||
await syncScriptLabels(github, owner, repo, pullNumber, currentLabels, labelsToApply, core);
|
||||
};
|
||||
|
||||
module.exports.computeScriptLabels = computeScriptLabels;
|
||||
module.exports.LABELS = LABELS;
|
||||
module.exports.SCRIPT_MANAGED_LABELS = SCRIPT_MANAGED_LABELS;
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync repo-root skills/ from harness-local SKILL.md files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
ROOT_SKILLS_DIR = REPO_ROOT / "skills"
|
||||
|
||||
|
||||
def _canonical_skill_id(source: Path) -> str:
|
||||
rel = source.relative_to(REPO_ROOT)
|
||||
parts = rel.parts
|
||||
if "cli_anything" in parts:
|
||||
package_index = parts.index("cli_anything") + 1
|
||||
if package_index < len(parts):
|
||||
package_name = parts[package_index]
|
||||
return f"cli-anything-{package_name.replace('_', '-')}"
|
||||
|
||||
software_dir = parts[0]
|
||||
return f"cli-anything-{software_dir.replace('_', '-')}"
|
||||
|
||||
|
||||
def _rewrite_name_frontmatter(content: str, skill_id: str) -> str:
|
||||
if not content.startswith("---\n"):
|
||||
return content
|
||||
|
||||
parts = content.split("---\n", 2)
|
||||
if len(parts) < 3:
|
||||
return content
|
||||
|
||||
_, frontmatter, body = parts
|
||||
lines = frontmatter.splitlines(keepends=True)
|
||||
rewritten: list[str] = []
|
||||
replaced = False
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if not replaced and line.startswith("name:"):
|
||||
rewritten.append(f'name: "{skill_id}"\n')
|
||||
replaced = True
|
||||
i += 1
|
||||
while i < len(lines) and (lines[i].startswith(" ") or lines[i].startswith("\t")):
|
||||
i += 1
|
||||
continue
|
||||
rewritten.append(line)
|
||||
i += 1
|
||||
|
||||
if not replaced:
|
||||
rewritten.insert(0, f'name: "{skill_id}"\n')
|
||||
|
||||
frontmatter = "".join(rewritten)
|
||||
return f"---\n{frontmatter}---\n{body}"
|
||||
|
||||
|
||||
def _discover_sources() -> list[Path]:
|
||||
sources: list[Path] = []
|
||||
sources.extend(sorted(REPO_ROOT.glob("*/agent-harness/cli_anything/*/skills/SKILL.md")))
|
||||
sources.extend(sorted(REPO_ROOT.glob("*/agent-harness/cli_anything/*/SKILL.md")))
|
||||
return [path for path in sources if path.is_file()]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sources = _discover_sources()
|
||||
ROOT_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
written = 0
|
||||
for source in sources:
|
||||
skill_id = _canonical_skill_id(source)
|
||||
target = ROOT_SKILLS_DIR / skill_id / "SKILL.md"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = source.read_text(encoding="utf-8")
|
||||
expected = _rewrite_name_frontmatter(content, skill_id)
|
||||
|
||||
if target.is_file() and target.read_text(encoding="utf-8") == expected:
|
||||
continue
|
||||
|
||||
# Overwriting is destructive: the mirror may hold edits the source lacks.
|
||||
# Say so, so the loss is never silent. validate_root_skills.py refuses
|
||||
# in that direction before CI ever reaches here.
|
||||
action = "overwrite" if target.is_file() else "create"
|
||||
print(f"{action}: {target.relative_to(REPO_ROOT)} <- {source.relative_to(REPO_ROOT)}")
|
||||
target.write_text(expected, encoding="utf-8")
|
||||
written += 1
|
||||
|
||||
print(f"Synced {written} root skill(s); {len(sources) - written} already up to date.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,270 @@
|
||||
[
|
||||
{
|
||||
"number": 262,
|
||||
"title": "Add MseeP.ai badge",
|
||||
"files": [
|
||||
{"filename": "README.md", "status": "modified"}
|
||||
],
|
||||
"expected": ["documentation"]
|
||||
},
|
||||
{
|
||||
"number": 260,
|
||||
"title": "feat: add hacker-feeds-cli to registry",
|
||||
"files": [
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 259,
|
||||
"title": "feat: add ve-twini to registry",
|
||||
"files": [
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 258,
|
||||
"title": "feat: add sliver to registry",
|
||||
"files": [
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 256,
|
||||
"title": "feat(Calibre): add a CLI-Anything harness for Calibre Desktop",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "README.md", "status": "modified"},
|
||||
{"filename": "calibre/agent-harness/CALIBRE.md", "status": "added"},
|
||||
{"filename": "calibre/agent-harness/cli_anything/calibre/calibre_cli.py", "status": "added"},
|
||||
{"filename": "calibre/agent-harness/cli_anything/calibre/core/session.py", "status": "added"},
|
||||
{"filename": "calibre/agent-harness/cli_anything/calibre/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "calibre/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "cli-anything-plugin/scripts/setup-cli-anything.sh", "status": "modified"},
|
||||
{"filename": "codex-skill/scripts/install.sh", "status": "modified"},
|
||||
{"filename": "registry.json", "status": "modified"},
|
||||
{"filename": "skills/cli-anything-calibre/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 254,
|
||||
"title": "feat: add quietshrink harness - Apple Silicon screen recording compressor",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "quietshrink/agent-harness/QUIETSHRINK.md", "status": "added"},
|
||||
{"filename": "quietshrink/agent-harness/cli_anything/quietshrink/quietshrink_cli.py", "status": "added"},
|
||||
{"filename": "quietshrink/agent-harness/cli_anything/quietshrink/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "quietshrink/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "registry.json", "status": "modified"},
|
||||
{"filename": "skills/cli-anything-quietshrink/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 252,
|
||||
"title": "Add cli-anything-rekordbox: Pioneer Rekordbox 6/7 harness",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "ableton/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "ableton/agent-harness/cli_anything/ableton/ableton_cli.py", "status": "added"},
|
||||
{"filename": "rekordbox/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "rekordbox/agent-harness/cli_anything/rekordbox/rekordbox_cli.py", "status": "added"},
|
||||
{"filename": "rekordbox/agent-harness/cli_anything/rekordbox/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "serum/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "vital/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "skills/cli-anything-rekordbox/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 251,
|
||||
"title": "feat: add s&box CLI harness",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "README.md", "status": "modified"},
|
||||
{"filename": "registry.json", "status": "modified"},
|
||||
{"filename": "sbox/agent-harness/SBOX.md", "status": "added"},
|
||||
{"filename": "sbox/agent-harness/cli_anything/sbox/sbox_cli.py", "status": "added"},
|
||||
{"filename": "sbox/agent-harness/cli_anything/sbox/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "sbox/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "skills/cli-anything-sbox/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 245,
|
||||
"title": "feat: mature lldb agent harness",
|
||||
"files": [
|
||||
{"filename": "lldb/agent-harness/LLDB.md", "status": "modified"},
|
||||
{"filename": "lldb/agent-harness/cli_anything/lldb/lldb_cli.py", "status": "modified"},
|
||||
{"filename": "lldb/agent-harness/cli_anything/lldb/skills/SKILL.md", "status": "modified"},
|
||||
{"filename": "lldb/agent-harness/setup.py", "status": "modified"},
|
||||
{"filename": "registry.json", "status": "modified"},
|
||||
{"filename": "skills/cli-anything-lldb/SKILL.md", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 244,
|
||||
"title": "feat: improve Unreal Insights live analysis",
|
||||
"files": [
|
||||
{"filename": "skills/cli-anything-unrealinsights/SKILL.md", "status": "modified"},
|
||||
{"filename": "unrealinsights/agent-harness/UNREALINSIGHTS.md", "status": "modified"},
|
||||
{"filename": "unrealinsights/agent-harness/cli_anything/unrealinsights/core/analyze.py", "status": "added"},
|
||||
{"filename": "unrealinsights/agent-harness/cli_anything/unrealinsights/skills/SKILL.md", "status": "modified"},
|
||||
{"filename": "unrealinsights/agent-harness/cli_anything/unrealinsights/unrealinsights_cli.py", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-skill", "existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 241,
|
||||
"title": "Feat/add firefly iii cli",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "firefly-iii/agent-harness/README.md", "status": "added"},
|
||||
{"filename": "firefly-iii/agent-harness/cli_anything/firefly_iii/firefly_iii_cli.py", "status": "added"},
|
||||
{"filename": "firefly-iii/agent-harness/cli_anything/firefly_iii/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "firefly-iii/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "firefly-iii/agent-harness/skills/cli-anything-firefly-iii/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 240,
|
||||
"title": "fix(blender): align docs and render execute with real behavior",
|
||||
"files": [
|
||||
{"filename": "README.md", "status": "modified"},
|
||||
{"filename": "README_CN.md", "status": "modified"},
|
||||
{"filename": "blender/agent-harness/cli_anything/blender/README.md", "status": "modified"},
|
||||
{"filename": "blender/agent-harness/cli_anything/blender/blender_cli.py", "status": "modified"},
|
||||
{"filename": "blender/agent-harness/cli_anything/blender/core/render.py", "status": "modified"},
|
||||
{"filename": "blender/agent-harness/cli_anything/blender/utils/blender_backend.py", "status": "modified"}
|
||||
],
|
||||
"expected": ["existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 238,
|
||||
"title": "feat: add NSLogger CLI harness",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "README.md", "status": "modified"},
|
||||
{"filename": "nslogger/agent-harness/NSLOGGER.md", "status": "added"},
|
||||
{"filename": "nslogger/agent-harness/cli_anything/nslogger/nslogger_cli.py", "status": "added"},
|
||||
{"filename": "nslogger/agent-harness/cli_anything/nslogger/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "nslogger/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "registry.json", "status": "modified"},
|
||||
{"filename": "skills/cli-anything-nslogger/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 237,
|
||||
"title": "refactor(openclaw-skill->macrocli): rename, complete backends, add recorder and visual anchor",
|
||||
"files": [
|
||||
{"filename": "macrocli/SKILL.md", "status": "added"},
|
||||
{"filename": "macrocli/agent-harness/MACROCLI.md", "status": "added"},
|
||||
{"filename": "macrocli/agent-harness/cli_anything/macrocli/macrocli_cli.py", "status": "added"},
|
||||
{"filename": "macrocli/agent-harness/cli_anything/macrocli/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "macrocli/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "openclaw-skill/agent-harness/cli_anything/openclaw/openclaw_cli.py", "status": "modified"},
|
||||
{"filename": "registry.json", "status": "modified"},
|
||||
{"filename": "skills/cli-anything-macrocli/SKILL.md", "status": "added"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 233,
|
||||
"title": "fix(browser): add MCP timeout guard to prevent fs hangs",
|
||||
"files": [
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/browser_cli.py", "status": "modified"},
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/skills/SKILL.md", "status": "modified"},
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/utils/domshell_backend.py", "status": "modified"},
|
||||
{"filename": "browser/agent-harness/setup.py", "status": "modified"},
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 220,
|
||||
"title": "fix(browser): harden bridge and sync REPL template",
|
||||
"files": [
|
||||
{"filename": ".github/workflows/pr-ci.yml", "status": "added"},
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/browser_cli.py", "status": "modified"},
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/utils/repl_skin.py", "status": "modified"},
|
||||
{"filename": "cli-anything-plugin/repl_skin.py", "status": "modified"},
|
||||
{"filename": "gimp/agent-harness/cli_anything/gimp/utils/repl_skin.py", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-skill", "existing-cli-fix", "github-actions"]
|
||||
},
|
||||
{
|
||||
"number": 218,
|
||||
"title": "fix: Executing cli-anything-browser --json fs ls yields no return result",
|
||||
"files": [
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/tests/test_core.py", "status": "modified"},
|
||||
{"filename": "browser/agent-harness/cli_anything/browser/utils/domshell_backend.py", "status": "modified"}
|
||||
],
|
||||
"expected": ["existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 202,
|
||||
"title": "security: add user confirmation guard to meta-skill installation flow",
|
||||
"files": [
|
||||
{"filename": "cli-hub-meta-skill/SKILL.md", "status": "modified"},
|
||||
{"filename": "docs/hub/SKILL.md", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill"]
|
||||
},
|
||||
{
|
||||
"number": 189,
|
||||
"title": "feat: add MiniMax CLI harness (chat + TTS)",
|
||||
"files": [
|
||||
{"filename": ".gitignore", "status": "modified"},
|
||||
{"filename": "minimax/agent-harness/cli_anything/minimax/minimax_cli.py", "status": "added"},
|
||||
{"filename": "minimax/agent-harness/cli_anything/minimax/skills/SKILL.md", "status": "added"},
|
||||
{"filename": "minimax/agent-harness/setup.py", "status": "added"},
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "new-cli"]
|
||||
},
|
||||
{
|
||||
"number": 187,
|
||||
"title": "Refactor AnyGen CLI for better state management",
|
||||
"files": [
|
||||
{"filename": "anygen/agent-harness/cli_anything/anygen/anygen_cli.py", "status": "modified"}
|
||||
],
|
||||
"expected": ["existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 167,
|
||||
"title": "feat(freecad): add FreeCAD 1.0.2 backward compatibility",
|
||||
"files": [
|
||||
{"filename": "freecad/agent-harness/FREECAD.md", "status": "modified"},
|
||||
{"filename": "freecad/agent-harness/cli_anything/freecad/core/generate.py", "status": "modified"},
|
||||
{"filename": "freecad/agent-harness/cli_anything/freecad/freecad_cli.py", "status": "modified"},
|
||||
{"filename": "freecad/agent-harness/cli_anything/freecad/skills/SKILL.md", "status": "modified"},
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub", "cli-anything-skill", "existing-cli-fix"]
|
||||
},
|
||||
{
|
||||
"number": 106,
|
||||
"title": "fix: split token-aware into default exclusions + optional --economic mode",
|
||||
"files": [
|
||||
{"filename": "cli-anything-plugin/HARNESS.md", "status": "modified"},
|
||||
{"filename": "cli-anything-plugin/commands/cli-anything.md", "status": "modified"},
|
||||
{"filename": "cli-anything-plugin/commands/refine.md", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-skill"]
|
||||
},
|
||||
{
|
||||
"number": "registry-maintenance",
|
||||
"title": "Update registry dates",
|
||||
"files": [
|
||||
{"filename": "registry.json", "status": "modified"}
|
||||
],
|
||||
"expected": ["cli-anything-hub"]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,208 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
|
||||
const labeler = require("../pr-labeler.js");
|
||||
const fixtures = require("./pr-labeler-fixtures.json");
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, "../../..");
|
||||
const LABELER_CONFIG_PATH = path.join(REPO_ROOT, ".github/labeler.yml");
|
||||
const EXPECTED_LABELS = [
|
||||
"cli-anything-hub",
|
||||
"cli-anything-skill",
|
||||
"documentation",
|
||||
"existing-cli-fix",
|
||||
"github-actions",
|
||||
"new-cli",
|
||||
];
|
||||
|
||||
function normalizeLabels(labels) {
|
||||
return [...labels].sort();
|
||||
}
|
||||
|
||||
function sameLabels(left, right) {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
function loadLabelerPatterns() {
|
||||
const patternsByLabel = new Map();
|
||||
let currentLabel = null;
|
||||
|
||||
for (const line of fs.readFileSync(LABELER_CONFIG_PATH, "utf8").split(/\r?\n/)) {
|
||||
const labelMatch = line.match(/^([a-z0-9-]+):$/);
|
||||
if (labelMatch) {
|
||||
currentLabel = labelMatch[1];
|
||||
patternsByLabel.set(currentLabel, []);
|
||||
continue;
|
||||
}
|
||||
|
||||
const patternMatch = line.match(/-\s+"([^"]+)"/);
|
||||
if (currentLabel && patternMatch) {
|
||||
patternsByLabel.get(currentLabel).push(patternMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return patternsByLabel;
|
||||
}
|
||||
|
||||
function globToRegex(pattern) {
|
||||
let regex = "^";
|
||||
|
||||
for (let index = 0; index < pattern.length; index += 1) {
|
||||
const char = pattern[index];
|
||||
const next = pattern[index + 1];
|
||||
const afterNext = pattern[index + 2];
|
||||
|
||||
if (char === "*" && next === "*" && afterNext === "/") {
|
||||
regex += "(?:.*/)?";
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "*" && next === "*") {
|
||||
regex += ".*";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "*") {
|
||||
regex += "[^/]*";
|
||||
continue;
|
||||
}
|
||||
|
||||
regex += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
||||
}
|
||||
|
||||
regex += "$";
|
||||
return new RegExp(regex);
|
||||
}
|
||||
|
||||
function computePathLabels(files) {
|
||||
const labels = new Set();
|
||||
const patternsByLabel = loadLabelerPatterns();
|
||||
|
||||
for (const [label, patterns] of patternsByLabel.entries()) {
|
||||
const regexes = patterns.map(globToRegex);
|
||||
if (files.some((file) => regexes.some((regex) => regex.test(file.filename)))) {
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
function computeAllLabels(sample) {
|
||||
const labels = new Set(labeler.computeScriptLabels(sample.files, sample.title));
|
||||
for (const pathLabel of computePathLabels(sample.files)) {
|
||||
labels.add(pathLabel);
|
||||
}
|
||||
return normalizeLabels(labels);
|
||||
}
|
||||
|
||||
function summarizeMetrics(results) {
|
||||
const labels = new Set(EXPECTED_LABELS);
|
||||
for (const result of results) {
|
||||
for (const label of result.expected) labels.add(label);
|
||||
for (const label of result.predicted) labels.add(label);
|
||||
}
|
||||
|
||||
const perLabel = {};
|
||||
for (const label of labels) {
|
||||
let truePositive = 0;
|
||||
let falsePositive = 0;
|
||||
let falseNegative = 0;
|
||||
|
||||
for (const result of results) {
|
||||
const expected = new Set(result.expected);
|
||||
const predicted = new Set(result.predicted);
|
||||
|
||||
if (expected.has(label) && predicted.has(label)) truePositive += 1;
|
||||
if (!expected.has(label) && predicted.has(label)) falsePositive += 1;
|
||||
if (expected.has(label) && !predicted.has(label)) falseNegative += 1;
|
||||
}
|
||||
|
||||
const precisionDenominator = truePositive + falsePositive;
|
||||
const recallDenominator = truePositive + falseNegative;
|
||||
|
||||
perLabel[label] = {
|
||||
truePositive,
|
||||
falsePositive,
|
||||
falseNegative,
|
||||
precision: precisionDenominator === 0 ? 1 : truePositive / precisionDenominator,
|
||||
recall: recallDenominator === 0 ? 1 : truePositive / recallDenominator,
|
||||
};
|
||||
}
|
||||
|
||||
const exactMatches = results.filter((result) => sameLabels(result.predicted, result.expected)).length;
|
||||
|
||||
return {
|
||||
exactAccuracy: exactMatches / results.length,
|
||||
perLabel,
|
||||
};
|
||||
}
|
||||
|
||||
test("real PR fixture label accuracy and recall", () => {
|
||||
const results = fixtures.map((sample) => ({
|
||||
number: sample.number,
|
||||
title: sample.title,
|
||||
expected: normalizeLabels(sample.expected),
|
||||
predicted: computeAllLabels(sample),
|
||||
}));
|
||||
|
||||
const mismatches = results.filter((result) => {
|
||||
return !sameLabels(result.predicted, result.expected);
|
||||
});
|
||||
|
||||
const metrics = summarizeMetrics(results);
|
||||
|
||||
assert.deepStrictEqual(mismatches, [], JSON.stringify({mismatches, metrics}, null, 2));
|
||||
assert.equal(metrics.exactAccuracy, 1);
|
||||
|
||||
for (const [label, metric] of Object.entries(metrics.perLabel)) {
|
||||
assert.equal(metric.precision, 1, `${label} precision: ${JSON.stringify(metric)}`);
|
||||
assert.equal(metric.recall, 1, `${label} recall: ${JSON.stringify(metric)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("labeler config and script agree on the supported label set", () => {
|
||||
const configLabels = new Set(loadLabelerPatterns().keys());
|
||||
const scriptLabels = new Set(Object.keys(labeler.LABELS));
|
||||
for (const label of EXPECTED_LABELS) {
|
||||
assert(scriptLabels.has(label), `${label} must be creatable by pr-labeler.js`);
|
||||
}
|
||||
|
||||
assert(configLabels.has("cli-anything-hub"));
|
||||
assert(configLabels.has("cli-anything-skill"));
|
||||
assert(configLabels.has("github-actions"));
|
||||
});
|
||||
|
||||
test("registry-only maintenance is not treated as a new CLI", () => {
|
||||
const labels = computeAllLabels({
|
||||
title: "Update registry dates",
|
||||
files: [{filename: "registry.json", status: "modified"}],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(labels, ["cli-anything-hub"]);
|
||||
});
|
||||
|
||||
test("mixed README and harness changes are not documentation-only", () => {
|
||||
const labels = computeAllLabels({
|
||||
title: "fix(blender): update docs and render behavior",
|
||||
files: [
|
||||
{filename: "README.md", status: "modified"},
|
||||
{filename: "blender/agent-harness/cli_anything/blender/core/render.py", status: "modified"},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(labels, ["existing-cli-fix"]);
|
||||
});
|
||||
|
||||
test("harness README-only changes are documentation", () => {
|
||||
const labels = computeAllLabels({
|
||||
title: "docs(firefly-iii): clarify setup notes",
|
||||
files: [{filename: "firefly-iii/agent-harness/README.md", status: "modified"}],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(labels, ["documentation"]);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "update_registry_dates.py"
|
||||
SPEC = importlib.util.spec_from_file_location("update_registry_dates", SCRIPT_PATH)
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC and SPEC.loader
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
def test_resolve_harness_path_prefers_install_subdirectory_for_qgis():
|
||||
cli = {
|
||||
"name": "qgis",
|
||||
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=QGIS/agent-harness",
|
||||
"skill_md": "QGIS/agent-harness/cli_anything/qgis/skills/SKILL.md",
|
||||
}
|
||||
|
||||
path = MODULE.resolve_harness_path(cli, MODULE.REPO_ROOT)
|
||||
|
||||
assert path == MODULE.REPO_ROOT / "QGIS" / "agent-harness"
|
||||
|
||||
|
||||
def test_resolve_harness_path_handles_underscore_directory_names():
|
||||
cli = {
|
||||
"name": "unimol_tools",
|
||||
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=unimol_tools/agent-harness",
|
||||
"skill_md": "skills/cli-anything-unimol-tools/SKILL.md",
|
||||
}
|
||||
|
||||
path = MODULE.resolve_harness_path(cli, MODULE.REPO_ROOT)
|
||||
|
||||
assert path == MODULE.REPO_ROOT / "unimol_tools" / "agent-harness"
|
||||
|
||||
|
||||
def test_extract_external_source_url_from_cargo_git_install():
|
||||
cli = {
|
||||
"name": "clibrowser",
|
||||
"install_cmd": "cargo install --git https://github.com/allthingssecurity/clibrowser.git --tag v0.1.0 --locked",
|
||||
"source_url": None,
|
||||
}
|
||||
|
||||
source_url = MODULE.extract_external_source_url(cli)
|
||||
|
||||
assert source_url == "https://github.com/allthingssecurity/clibrowser"
|
||||
|
||||
|
||||
def test_extract_npm_package_supports_scoped_package_names():
|
||||
cli = {
|
||||
"npm_package": "@sentry/cli",
|
||||
"install_cmd": "npm install -g @sentry/cli",
|
||||
}
|
||||
|
||||
assert MODULE._extract_npm_package(cli) == "@sentry/cli"
|
||||
|
||||
|
||||
def test_extract_pypi_package_supports_python_module_invocation():
|
||||
install_cmd = "python -m pip install py4csr"
|
||||
|
||||
assert MODULE._extract_pypi_package(install_cmd) == "py4csr"
|
||||
|
||||
|
||||
def test_extract_pypi_package_skips_index_option_values():
|
||||
install_cmd = (
|
||||
"python -m pip install --index-url https://mirror.example/simple "
|
||||
"--trusted-host mirror.example py4csr"
|
||||
)
|
||||
|
||||
assert MODULE._extract_pypi_package(install_cmd) == "py4csr"
|
||||
|
||||
|
||||
def test_extract_pypi_package_skips_editable_option_value():
|
||||
install_cmd = "pip install -e ./local/pkg py4csr"
|
||||
|
||||
assert MODULE._extract_pypi_package(install_cmd) == "py4csr"
|
||||
|
||||
|
||||
def test_extract_pypi_package_returns_none_for_editable_only_install():
|
||||
install_cmd = "pip install -e ."
|
||||
|
||||
assert MODULE._extract_pypi_package(install_cmd) is None
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Update registry-dates.json with meaningful per-CLI update dates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
USER_AGENT = "CLI-Anything registry date updater"
|
||||
GITHUB_REPO_RE = re.compile(r"https://github\.com/([^/]+/[^/#?]+?)(?:\.git)?(?:[/?#].*)?$")
|
||||
GIT_URL_RE = re.compile(r"https://github\.com/[^\s#]+")
|
||||
SUBDIRECTORY_RE = re.compile(r"#subdirectory=([^\s]+)")
|
||||
PIP_OPTIONS_WITH_VALUES = {
|
||||
"-c",
|
||||
"--constraint",
|
||||
"-r",
|
||||
"--requirement",
|
||||
"-i",
|
||||
"--index-url",
|
||||
"--extra-index-url",
|
||||
"-e",
|
||||
"--editable",
|
||||
"-f",
|
||||
"--find-links",
|
||||
"--trusted-host",
|
||||
"--python-version",
|
||||
"--platform",
|
||||
"--implementation",
|
||||
"--abi",
|
||||
"--root",
|
||||
"--prefix",
|
||||
"--src",
|
||||
"--target",
|
||||
"--upgrade-strategy",
|
||||
"-C",
|
||||
"--config-settings",
|
||||
"--cert",
|
||||
"--client-cert",
|
||||
"--cache-dir",
|
||||
"--log",
|
||||
"--report",
|
||||
}
|
||||
|
||||
|
||||
def _fetch_json(url: str) -> dict | None:
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_last_modified(url: str) -> str | None:
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}, method="HEAD")
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
last_modified = resp.headers.get("Last-Modified")
|
||||
if not last_modified:
|
||||
return None
|
||||
return parsedate_to_datetime(last_modified).astimezone(timezone.utc).strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _git_log_timestamp(target_path: Path, excluded_globs: tuple[str, ...] = ()) -> int | None:
|
||||
try:
|
||||
relative_target = target_path.relative_to(REPO_ROOT).as_posix()
|
||||
cmd = ["git", "log", "-1", "--format=%ct", "--", relative_target]
|
||||
cmd.extend(f":(exclude,glob){pattern}" for pattern in excluded_globs)
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=REPO_ROOT,
|
||||
)
|
||||
return int(result.stdout.strip())
|
||||
except (subprocess.CalledProcessError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def get_last_modified(target_path: Path) -> str | None:
|
||||
"""Get the most recent git commit date for CLI-specific files in a repo path."""
|
||||
relative_target = target_path.relative_to(REPO_ROOT).as_posix()
|
||||
shared_file_globs = (
|
||||
f"{relative_target}/cli_anything/**/utils/repl_skin.py",
|
||||
f"{relative_target}/cli_anything/**/skills/SKILL.md",
|
||||
f"{relative_target}/cli_anything/**/SKILL.md",
|
||||
)
|
||||
|
||||
timestamp = _git_log_timestamp(target_path, excluded_globs=shared_file_globs)
|
||||
if timestamp is None:
|
||||
timestamp = _git_log_timestamp(target_path)
|
||||
if timestamp is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
except (OverflowError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def get_github_repo_date(source_url: str) -> str | None:
|
||||
"""Get the last push date from a GitHub repo via the API."""
|
||||
match = GITHUB_REPO_RE.match(source_url)
|
||||
if not match:
|
||||
return None
|
||||
repo_slug = match.group(1)
|
||||
data = _fetch_json(f"https://api.github.com/repos/{repo_slug}")
|
||||
if not data:
|
||||
return None
|
||||
pushed_at = data.get("pushed_at")
|
||||
return pushed_at[:10] if pushed_at else None
|
||||
|
||||
|
||||
def _extract_pypi_package(install_cmd: str) -> str | None:
|
||||
if not install_cmd:
|
||||
return None
|
||||
try:
|
||||
tokens = shlex.split(install_cmd)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if not tokens:
|
||||
return None
|
||||
|
||||
install_index = None
|
||||
if (
|
||||
len(tokens) >= 3
|
||||
and tokens[0] in {"python", "python3"}
|
||||
and tokens[1:3] == ["-m", "pip"]
|
||||
):
|
||||
install_index = 3
|
||||
elif tokens[0] in {"pip", "pip3"}:
|
||||
install_index = 1
|
||||
|
||||
if install_index is None or install_index >= len(tokens) or tokens[install_index] != "install":
|
||||
return None
|
||||
|
||||
index = install_index + 1
|
||||
while index < len(tokens):
|
||||
token = tokens[index]
|
||||
if token.startswith("-"):
|
||||
option_name = token.split("=", 1)[0]
|
||||
if option_name in PIP_OPTIONS_WITH_VALUES and "=" not in token:
|
||||
index += 2
|
||||
continue
|
||||
index += 1
|
||||
continue
|
||||
if "://" in token or token.startswith("git+"):
|
||||
return None
|
||||
return token
|
||||
return None
|
||||
|
||||
|
||||
def get_pypi_date(install_cmd: str) -> str | None:
|
||||
"""Get the last release date from PyPI for a pip-installable package."""
|
||||
package = _extract_pypi_package(install_cmd)
|
||||
if not package:
|
||||
return None
|
||||
data = _fetch_json(f"https://pypi.org/pypi/{package}/json")
|
||||
if not data:
|
||||
return None
|
||||
latest = data.get("info", {}).get("version")
|
||||
releases = data.get("releases", {})
|
||||
release_files = releases.get(latest or "", [])
|
||||
if not release_files:
|
||||
return None
|
||||
upload_time = release_files[0].get("upload_time") or release_files[0].get("upload_time_iso_8601")
|
||||
return upload_time[:10] if upload_time else None
|
||||
|
||||
|
||||
def _extract_npm_package(cli: dict) -> str | None:
|
||||
package = cli.get("npm_package")
|
||||
if package:
|
||||
return package
|
||||
|
||||
install_cmd = cli.get("install_cmd", "")
|
||||
match = re.search(r"npm install -g (\S+)", install_cmd)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def get_npm_date(cli: dict) -> str | None:
|
||||
"""Get the latest publish date from the npm registry."""
|
||||
package = _extract_npm_package(cli)
|
||||
if not package:
|
||||
return None
|
||||
encoded = urllib.parse.quote(package, safe="")
|
||||
data = _fetch_json(f"https://registry.npmjs.org/{encoded}")
|
||||
if not data:
|
||||
return None
|
||||
latest = data.get("dist-tags", {}).get("latest")
|
||||
published = data.get("time", {}).get(latest or "")
|
||||
return published[:10] if published else None
|
||||
|
||||
|
||||
def _extract_install_subdirectory(cli: dict) -> str | None:
|
||||
install_cmd = cli.get("install_cmd") or ""
|
||||
match = SUBDIRECTORY_RE.search(install_cmd)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def _extract_skill_subdirectory(cli: dict) -> str | None:
|
||||
skill_md = cli.get("skill_md")
|
||||
if not skill_md or skill_md.startswith("http"):
|
||||
return None
|
||||
marker = "/agent-harness/"
|
||||
if marker not in skill_md:
|
||||
return None
|
||||
return skill_md.split(marker, 1)[0] + marker.rstrip("/")
|
||||
|
||||
|
||||
def resolve_harness_path(cli: dict, repo_root: Path) -> Path | None:
|
||||
"""Resolve the on-disk harness path for an in-repo CLI entry."""
|
||||
for relative in (_extract_install_subdirectory(cli), _extract_skill_subdirectory(cli)):
|
||||
if relative:
|
||||
candidate = repo_root / relative
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
|
||||
candidate_dirs = []
|
||||
for name in (cli.get("name"), cli.get("name", "").replace("-", "_"), cli.get("name", "").replace("_", "-")):
|
||||
if name and name not in candidate_dirs:
|
||||
candidate_dirs.append(name)
|
||||
|
||||
for directory in candidate_dirs:
|
||||
candidate = repo_root / directory / "agent-harness"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def extract_external_source_url(cli: dict) -> str | None:
|
||||
"""Best-effort source URL discovery for third-party CLIs."""
|
||||
source_url = cli.get("source_url")
|
||||
if source_url:
|
||||
return source_url
|
||||
|
||||
install_cmd = cli.get("install_cmd") or ""
|
||||
git_match = GIT_URL_RE.search(install_cmd)
|
||||
if git_match:
|
||||
return git_match.group(0).removesuffix(".git")
|
||||
|
||||
for field in ("homepage", "docs_url"):
|
||||
value = cli.get(field)
|
||||
if value and "github.com/" in value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def get_external_date(cli: dict) -> str | None:
|
||||
"""Get a useful update date for external/public CLIs."""
|
||||
source_url = extract_external_source_url(cli)
|
||||
if source_url:
|
||||
date = get_github_repo_date(source_url)
|
||||
if date:
|
||||
return date
|
||||
|
||||
package_manager = (cli.get("package_manager") or "").lower()
|
||||
if package_manager == "npm":
|
||||
date = get_npm_date(cli)
|
||||
if date:
|
||||
return date
|
||||
|
||||
date = get_pypi_date(cli.get("install_cmd", ""))
|
||||
if date:
|
||||
return date
|
||||
|
||||
for field in ("homepage", "docs_url"):
|
||||
url = cli.get(field)
|
||||
if url:
|
||||
date = _fetch_last_modified(url)
|
||||
if date:
|
||||
return date
|
||||
return None
|
||||
|
||||
|
||||
def get_cli_date(cli: dict, repo_root: Path) -> str | None:
|
||||
harness_path = resolve_harness_path(cli, repo_root)
|
||||
if harness_path:
|
||||
return get_last_modified(harness_path)
|
||||
return get_external_date(cli)
|
||||
|
||||
|
||||
def _load_registry(path: Path) -> list[dict]:
|
||||
with path.open(encoding="utf-8") as f:
|
||||
return json.load(f)["clis"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dates_path = REPO_ROOT / "docs" / "hub" / "registry-dates.json"
|
||||
all_clis = _load_registry(REPO_ROOT / "registry.json") + _load_registry(REPO_ROOT / "public_registry.json")
|
||||
|
||||
dates = {cli["name"]: get_cli_date(cli, REPO_ROOT) for cli in all_clis}
|
||||
|
||||
with dates_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(dates, f, indent=2)
|
||||
|
||||
print(f"Updated dates for {len(dates)} CLI entries")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate that deep harness SKILL.md files are mirrored in repo-root skills/."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _mirror_only_lines(expected: str, actual: str) -> list[str]:
|
||||
"""Lines present in the root mirror but absent from the regenerated source.
|
||||
|
||||
These are exactly what `sync_root_skills.py` would overwrite and destroy,
|
||||
because it regenerates the mirror from the harness source unconditionally.
|
||||
"""
|
||||
diff = difflib.unified_diff(
|
||||
expected.splitlines(), actual.splitlines(), lineterm="", n=0
|
||||
)
|
||||
return [
|
||||
line[1:].strip()
|
||||
for line in diff
|
||||
if line.startswith("+") and not line.startswith("+++") and line[1:].strip()
|
||||
]
|
||||
|
||||
|
||||
def _load_sync_helpers():
|
||||
namespace: dict[str, object] = {"__file__": str(REPO_ROOT / ".github" / "scripts" / "sync_root_skills.py")}
|
||||
sync_script = REPO_ROOT / ".github" / "scripts" / "sync_root_skills.py"
|
||||
exec(sync_script.read_text(encoding="utf-8"), namespace)
|
||||
return namespace
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sync = _load_sync_helpers()
|
||||
discover_sources = sync["_discover_sources"]
|
||||
canonical_skill_id = sync["_canonical_skill_id"]
|
||||
rewrite_name_frontmatter = sync["_rewrite_name_frontmatter"]
|
||||
root_skills_dir = sync["ROOT_SKILLS_DIR"]
|
||||
|
||||
# Drift where the mirror holds content the source lacks. Regenerating would
|
||||
# delete it, so these must be resolved by editing the source instead.
|
||||
clobber: list[tuple[Path, Path, list[str]]] = []
|
||||
# Drift the sync script can safely repair: the source moved ahead.
|
||||
stale: list[str] = []
|
||||
|
||||
for source in discover_sources():
|
||||
skill_id = canonical_skill_id(source)
|
||||
target = root_skills_dir / skill_id / "SKILL.md"
|
||||
if not target.is_file():
|
||||
stale.append(
|
||||
f"Missing root skill for {source.relative_to(REPO_ROOT)}: expected {target.relative_to(REPO_ROOT)}"
|
||||
)
|
||||
continue
|
||||
|
||||
source_content = source.read_text(encoding="utf-8")
|
||||
expected = rewrite_name_frontmatter(source_content, skill_id)
|
||||
actual = target.read_text(encoding="utf-8")
|
||||
if actual == expected:
|
||||
continue
|
||||
|
||||
mirror_only = _mirror_only_lines(expected, actual)
|
||||
if mirror_only:
|
||||
clobber.append((source, target, mirror_only))
|
||||
else:
|
||||
stale.append(
|
||||
f"Out-of-sync root skill for {source.relative_to(REPO_ROOT)}: {target.relative_to(REPO_ROOT)}"
|
||||
)
|
||||
|
||||
if not clobber and not stale:
|
||||
print("Root skills validation passed.")
|
||||
return 0
|
||||
|
||||
print("Root skills validation failed:", file=sys.stderr)
|
||||
|
||||
if clobber:
|
||||
print(
|
||||
"\nYou edited a GENERATED file. `skills/` is produced from the harness\n"
|
||||
"SKILL.md sources; running the sync script would DELETE these edits.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for source, target, mirror_only in clobber:
|
||||
print(f"\n- {target.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
print(
|
||||
f" contains content not present in {source.relative_to(REPO_ROOT)}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for line in mirror_only[:10]:
|
||||
print(f" + {line}", file=sys.stderr)
|
||||
if len(mirror_only) > 10:
|
||||
print(f" … and {len(mirror_only) - 10} more line(s)", file=sys.stderr)
|
||||
print(
|
||||
f" Fix: move the change into {source.relative_to(REPO_ROOT)},\n"
|
||||
f" then run `python3 .github/scripts/sync_root_skills.py`.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if stale:
|
||||
print("\nThe following root skills are stale and can be regenerated:", file=sys.stderr)
|
||||
for error in stale:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
print(
|
||||
"Run `python3 .github/scripts/sync_root_skills.py` and commit the updated root skills.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user