diff --git a/plugins/ditto/shared-skills.lock.json b/plugins/ditto/shared-skills.lock.json index 02780ef..733955e 100644 --- a/plugins/ditto/shared-skills.lock.json +++ b/plugins/ditto/shared-skills.lock.json @@ -1,11 +1,11 @@ { "schema_version": 1, "source_repository": "https://github.com/ditto-assistant/ditto-skills", - "source_commit": "9a2a3ee2af084fe2833cec7b093cfd371098f867", + "source_commit": "ee9da77c1502fb183e5eb6e7f265409e4af14b9e", "host": "plugins", "core_sha256": "5912af5483c469ec15db27858122fc9128fc25cd239202c0fdd9727de7dd5f6b", "adapter_sha256": "7cd895ab3feb0581e7e782de46b8b8220e4281a9ebfe6f6b651753e3a545bac1", - "generator_sha256": "6d23fa054bfa19203783fd7bdf6a6949915a5e62b6c71349af7031d5caae1500", + "generator_sha256": "c3cab1c78bcbd50b81bc80698d87567921326ee9886d220ff61c0d5bd18128fe", "skill_sha256": "dc4a46fc6de30ceac400ced65b122895738757cdcdaf1be529d63bec165e5835", "files_sha256": { "plugins/ditto/skills/curate-music/SKILL.md": "b498f8639aeb64e59f88830ac162a894f9f128b7e1f2db6a7c528092904c1eb4", diff --git a/scripts/shared_skills.py b/scripts/shared_skills.py index 737166b..92e0f34 100644 --- a/scripts/shared_skills.py +++ b/scripts/shared_skills.py @@ -43,6 +43,11 @@ ], } MANIFESTS = {host: f"plugins/{DEFAULT_PLUGIN}/{paths[0]}" for host, paths in PLUGIN_MANIFESTS.items()} +# Repository-root host manifests that point into one plugin, for hosts that only +# install from a repository root (Kimi Code reads /.kimi-plugin/plugin.json +# when given a GitHub URL). A plugin opts in with "root_manifests" in plugins.json; +# they are versioned and bumped with that plugin's own manifests. +ROOT_MANIFESTS = {"plugins": {".kimi-plugin/plugin.json"}} def digest(value): @@ -109,6 +114,13 @@ def plugin_registry(source): names = catalog_names(source, entry["catalog"]) if adapted is not None and names is not None and adapted not in names: raise ValueError("Adapted skill must belong to the plugin catalog") + roots = entry.get("root_manifests", []) + allowed = set().union(*ROOT_MANIFESTS.values()) + if not isinstance(roots, list) or any(not isinstance(r, str) or r not in allowed for r in roots) or len(set(roots)) != len(roots): + raise ValueError("Invalid plugin registry: root_manifests must list known repository-root host manifests") + claimed = [r for entry in data.values() for r in entry.get("root_manifests", [])] + if len(set(claimed)) != len(claimed): + raise ValueError("Invalid plugin registry: a root manifest can point into only one plugin") return data @@ -161,11 +173,19 @@ def additional_files(source, plugin=DEFAULT_PLUGIN, catalog="memory", adapted=DE return result -def bump_manifests(repo, host, plugin): - files = [file_at(repo, f"plugins/{plugin}/{rel}") for rel in PLUGIN_MANIFESTS[host]] - if not files[0].exists(): +def host_manifests(repo, host, plugin, root_manifests=()): + """Repository-relative versioned manifests of one plugin that exist in repo.""" + if any(r not in ROOT_MANIFESTS.get(host, set()) for r in root_manifests): + raise ValueError("Root manifests are not supported for this host") + paths = [f"plugins/{plugin}/{rel}" for rel in PLUGIN_MANIFESTS[host]] + list(root_manifests) + return [p for p in paths if file_at(repo, p).exists()] + + +def bump_manifests(repo, host, plugin, root_manifests=()): + if not file_at(repo, f"plugins/{plugin}/{PLUGIN_MANIFESTS[host][0]}").exists(): raise ValueError("Missing primary host manifest") - present = [(f, json.loads(f.read_text())) for f in files if f.exists()] + files = [file_at(repo, p) for p in host_manifests(repo, host, plugin, root_manifests)] + present = [(f, json.loads(f.read_text())) for f in files] if any("version" not in m for _, m in present): raise ValueError("Every host manifest in one plugin must carry a version before bumping") versions = {m["version"] for _, m in present} @@ -181,7 +201,7 @@ def bump_manifests(repo, host, plugin): return [f.relative_to(Path(repo).resolve()).as_posix() for f, _ in present] -def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog="memory", adapted=DEFAULT_ADAPTED): +def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog="memory", adapted=DEFAULT_ADAPTED, root_manifests=()): paths = layout(plugin, adapted) sha = source_identity(source) generator = file_at(source, GENERATOR).read_bytes() @@ -213,7 +233,7 @@ def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog return False output_changed = skill is not None and (not file_at(repo, paths["output"]).exists() or file_at(repo, paths["output"]).read_bytes() != skill) if bump_version and (output_changed or skill is None and not old or old.get("files_sha256", {}) != extra_hashes): - bump_manifests(repo, host, plugin) + bump_manifests(repo, host, plugin, root_manifests) # A renamed or dropped adapted skill leaves its previous output behind; # old locks without the key used the default adapted skill. if old: @@ -236,12 +256,20 @@ def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog def build_all(source, repo, host, bump_version=False): """Build every plugin in the source registry that the distribution hosts.""" changed = {} - for plugin, entry in plugin_registry(source).items(): + registry = plugin_registry(source) + if host == "plugins": + # A root manifest no plugin claims would silently keep its old version. + claimed = {r for entry in registry.values() for r in entry.get("root_manifests", [])} + for root in sorted(ROOT_MANIFESTS[host] - claimed): + if file_at(repo, root).exists(): + raise ValueError(f"{root} exists but no plugin in scripts/plugins.json lists it under root_manifests") + for plugin, entry in registry.items(): if host != "plugins" and plugin != DEFAULT_PLUGIN: continue # single-host repositories carry only the original plugin if not file_at(repo, f"plugins/{plugin}/{PLUGIN_MANIFESTS[host][0]}").exists(): raise ValueError(f"Distribution lacks the {plugin} plugin manifest; scaffold it before generating") - changed[plugin] = build(source, repo, host, bump_version, plugin, entry["catalog"], entry.get("adapted_skill")) + roots = entry.get("root_manifests", []) if host == "plugins" else [] + changed[plugin] = build(source, repo, host, bump_version, plugin, entry["catalog"], entry.get("adapted_skill"), roots) return changed