1 import Base 2 import bend-net-json@0.3.0.0/json.bend as Json 3 import ./json.bend as J 4 import ./type.bend as Pl 5 import ./claude/ops.bend as ClO 6 import ./codex/ops.bend as CxO 7 8 # A plugin's two manifests: the root plugin.json (Agent Plugins 1.0, which Codex reads) and 9 # .claude-plugin/plugin.json (which Claude Code reads). 10 11 def schema() -> String: 12 "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" 13 14 def version_show(v: Pl.Version) -> String: 15 match v: 16 case Pl.Version{major, minor, patch}: 17 Nat.show(major) ++ "." ++ Nat.show(minor) ++ "." ++ Nat.show(patch) 18 19 def author(a: Pl.Author) -> Json.Val: 20 match a: 21 case Pl.Author{name, email, url}: 22 J.cat([[("name", Json.Str{name})], J.opt("email", J.text(email)), J.opt("url", J.text(url))]) 23 24 def keywords(xs: List<&2, String>) -> List<&2, Sigma<&2, &2, String, _ => Json.Val>>: 25 match xs: 26 case []: 27 [] 28 case x <> rest: 29 [("keywords", J.texts(x <> rest))] 30 31 def core_keys(c: Pl.Core) -> List<&2, Sigma<&2, &2, String, _ => Json.Val>>: 32 match c: 33 case Pl.Core{name, v, description, a, homepage, repository, license, ks}: 34 List.concat(&2, Sigma<&2, &2, String, _ => Json.Val>, [ 35 [("name", Json.Str{name}), ("version", Json.Str{version_show(v)}), ("description", Json.Str{description}), ("author", author(a))], 36 J.opt("homepage", J.text(homepage)), J.opt("repository", J.text(repository)), J.opt("license", J.text(license)), keywords(ks)]) 37 38 # Codex's extension, or nothing when Codex adds nothing. 39 def extensions(kvs: List<&2, Sigma<&2, &2, String, _ => Json.Val>>) -> List<&2, Sigma<&2, &2, String, _ => Json.Val>>: 40 match kvs: 41 case []: 42 [] 43 case kv <> rest: 44 [("extensions", J.obj([("com.openai", J.obj(kv <> rest))]))] 45 46 # The root plugin.json's text. 47 def agent(p: Pl.Package) -> String: 48 match p: 49 case Pl.Package{c, _, cx}: 50 Json.encode(J.cat([[("$schema", Json.Str{schema()})], core_keys(c), extensions(CxO.keys(cx))])) ++ "\n" 51 52 # .claude-plugin/plugin.json's text. 53 def claude(p: Pl.Package) -> String: 54 match p: 55 case Pl.Package{c, cl, _}: 56 Json.encode(J.cat([core_keys(c), ClO.keys(cl)])) ++ "\n" 57 58 def name_of(p: Pl.Package) -> String: 59 match p: 60 case Pl.Package{Pl.Core{n, _, _, _, _, _, _, _}, _, _}: 61 n 62 63 # ---- the standard's rule for a name ---- 64 def name_char(+c: Char) -> Bool: 65 Char.is_lower(c) || Char.is_digit(c) || Char.is_eq(c, '.') || Char.is_eq(c, '-') 66 67 def edge_char(+c: Char) -> Bool: 68 Char.is_lower(c) || Char.is_digit(c) 69 70 def repeats(cs: List<&2, Char>, +prev: Char) -> Bool: 71 match cs: 72 case []: 73 False{} 74 case +c <> rest: 75 ((Char.is_eq(c, '-') || Char.is_eq(c, '.')) && Char.is_eq(c, prev)) || repeats(rest, c) 76 77 def all_name_chars(cs: List<&2, Char>) -> Bool: 78 match cs: 79 case []: 80 True{} 81 case +c <> rest: 82 name_char(c) && all_name_chars(rest) 83 84 def first_ok(cs: List<&2, Char>) -> Bool: 85 match cs: 86 case []: 87 False{} 88 case +c <> _: 89 edge_char(c) 90 91 # 1 to 64 of a-z, 0-9, . and -, starting and ending with a letter or digit, with no -- or .. 92 def name_ok(+n: String) -> Bool: 93 Nat.is_ge(String.length(n), 1n) && Nat.is_le(String.length(n), 64n) && all_name_chars(String.to_list(n)) && 94 first_ok(String.to_list(n)) && first_ok(String.to_list(String.reverse(n))) && Bool.not(repeats(String.to_list(n), ' '))