Skip to main content

systemprompt_models/bridge/
plugin_bundle.rs

1//! Plugin bundle contract: the `.claude-plugin/plugin.json` manifest shape and
2//! the well-formedness predicate every consumer shares.
3//!
4//! A *plugin bundle* is the installable artifact a host (Claude Code / Cowork)
5//! reads: a directory rooted on `.claude-plugin/plugin.json` plus the component
6//! files it ships (`skills/<n>/SKILL.md`, `agents/<n>.md`, `.mcp.json`, …).
7//! [`PluginManifest`] is that manifest; [`bundle_has_manifest`] is the single
8//! definition of "is this directory a well-formed bundle?" so the gateway
9//! serve path, the bridge sync, the CLI generator, and the marketplace export
10//! never drift on the contract.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use serde::{Deserialize, Serialize};
16
17pub const PLUGIN_MANIFEST_RELPATH: &str = ".claude-plugin/plugin.json";
18
19/// Manifest directory names accepted on a host, in preference order.
20///
21/// Cowork historically materialised the manifest under both the dotted and the
22/// undotted directory, so both are honoured when probing an installed bundle.
23pub const PLUGIN_MANIFEST_DIRS: &[&str] = &[".claude-plugin", "claude-plugin"];
24
25pub const PLUGIN_MANIFEST_FILE: &str = "plugin.json";
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PluginManifest {
29    pub name: String,
30    #[serde(default)]
31    pub description: String,
32    #[serde(default)]
33    pub version: String,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub author: Option<ManifestAuthor>,
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub hooks: Option<String>,
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub keywords: Vec<String>,
40    #[serde(
41        default,
42        rename = "installationPreference",
43        skip_serializing_if = "Option::is_none"
44    )]
45    pub installation_preference: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ManifestAuthor {
50    pub name: String,
51    #[serde(default, skip_serializing_if = "String::is_empty")]
52    pub email: String,
53}
54
55pub fn bundle_has_manifest<S: AsRef<str>>(paths: impl IntoIterator<Item = S>) -> bool {
56    paths
57        .into_iter()
58        .any(|path| path.as_ref() == PLUGIN_MANIFEST_RELPATH)
59}