@ryuhq/sdk 0.1.14 → 0.2.2

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.
Files changed (105) hide show
  1. package/README.md +69 -7
  2. package/dist/action.cjs +839 -0
  3. package/dist/action.d.cts +88 -0
  4. package/dist/action.d.ts +88 -0
  5. package/dist/action.js +8 -0
  6. package/dist/agent-plugin.cjs +46 -0
  7. package/dist/agent-plugin.d.cts +43 -33
  8. package/dist/agent-plugin.d.ts +43 -33
  9. package/dist/agent-plugin.js +1 -1
  10. package/dist/agent.cjs +16 -0
  11. package/dist/agent.d.cts +377 -1
  12. package/dist/agent.d.ts +377 -1
  13. package/dist/agent.js +4 -2
  14. package/dist/app-B0Z9Ew_R.d.cts +141 -0
  15. package/dist/app-C-BDJwfG.d.ts +141 -0
  16. package/dist/builder.cjs +1194 -0
  17. package/dist/builder.d.cts +217 -0
  18. package/dist/builder.d.ts +217 -0
  19. package/dist/builder.js +29 -0
  20. package/dist/chunk-4TPUZDTI.js +94 -0
  21. package/dist/chunk-BC3A7HMO.js +164 -0
  22. package/dist/chunk-FZSFZOIN.js +200 -0
  23. package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
  24. package/dist/chunk-HLKJZAFK.js +361 -0
  25. package/dist/chunk-IKEDLLFY.js +19 -0
  26. package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
  27. package/dist/{chunk-AO2KJRDD.js → chunk-NZKVOSC2.js} +192 -3
  28. package/dist/chunk-QYFUNJOH.js +83 -0
  29. package/dist/chunk-SN2QBJUF.js +148 -0
  30. package/dist/chunk-TLDPEGC7.js +21 -0
  31. package/dist/chunk-TXSHHZF2.js +0 -0
  32. package/dist/chunk-VLIRNNAE.js +154 -0
  33. package/dist/{chunk-MTUBUPIV.js → chunk-Z57QDDJR.js} +8 -227
  34. package/dist/chunk-ZTJWBRUL.js +282 -0
  35. package/dist/cli.cjs +347 -18
  36. package/dist/cli.js +128 -23
  37. package/dist/client-D5U6ssPc.d.cts +84 -0
  38. package/dist/client-D5U6ssPc.d.ts +84 -0
  39. package/dist/index.cjs +810 -86
  40. package/dist/index.d.cts +19 -634
  41. package/dist/index.d.ts +19 -634
  42. package/dist/index.js +82 -699
  43. package/dist/manifest.cjs +200 -4
  44. package/dist/manifest.d.cts +260 -5
  45. package/dist/manifest.d.ts +260 -5
  46. package/dist/manifest.js +15 -1
  47. package/dist/mcp/client.cjs +180 -0
  48. package/dist/mcp/client.d.cts +49 -0
  49. package/dist/mcp/client.d.ts +49 -0
  50. package/dist/mcp/client.js +10 -0
  51. package/dist/mcp/server.cjs +370 -0
  52. package/dist/mcp/server.d.cts +127 -0
  53. package/dist/mcp/server.d.ts +127 -0
  54. package/dist/mcp/server.js +9 -0
  55. package/dist/mcp.cjs +376 -0
  56. package/dist/mcp.d.cts +2 -0
  57. package/dist/mcp.d.ts +2 -0
  58. package/dist/mcp.js +17 -0
  59. package/dist/model.cjs +141 -0
  60. package/dist/model.d.cts +33 -0
  61. package/dist/model.d.ts +33 -0
  62. package/dist/model.js +18 -0
  63. package/dist/plugin.cjs +46 -0
  64. package/dist/plugin.d.cts +215 -0
  65. package/dist/plugin.d.ts +215 -0
  66. package/dist/plugin.js +8 -0
  67. package/dist/runnable.cjs +1438 -0
  68. package/dist/runnable.d.cts +274 -0
  69. package/dist/runnable.d.ts +274 -0
  70. package/dist/runnable.js +34 -0
  71. package/dist/{index-B6SkaAjJ.d.ts → tool-AjkdFvhE.d.ts} +88 -461
  72. package/dist/{index-BvAB5eMk.d.cts → tool-CgzW92O_.d.cts} +88 -461
  73. package/package.json +48 -3
  74. package/src/agent/loop.test.ts +4 -4
  75. package/src/agent/tools.ts +3 -3
  76. package/src/agent-plugin.test.ts +58 -0
  77. package/src/agent-plugin.ts +109 -36
  78. package/src/builder.ts +9 -0
  79. package/src/cli/dev.test.ts +26 -47
  80. package/src/cli/dev.ts +10 -2
  81. package/src/cli-security.test.ts +109 -0
  82. package/src/cli.ts +141 -25
  83. package/src/contracts-lockstep.test.ts +16 -2
  84. package/src/exports-lockstep.test.ts +93 -0
  85. package/src/generated/plugin-manifest.ts +322 -29
  86. package/src/index.ts +53 -0
  87. package/src/manifest-schema.test.ts +30 -1
  88. package/src/manifest.fixtures.test.ts +22 -6
  89. package/src/manifest.test.ts +165 -10
  90. package/src/manifest.ts +454 -188
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/server.ts +2 -1
  93. package/src/model/index.ts +22 -0
  94. package/src/plugin/ryu-plugin.ts +82 -0
  95. package/src/runnable/action.test.ts +128 -0
  96. package/src/runnable/action.ts +202 -0
  97. package/src/runnable/app.test.ts +2 -0
  98. package/src/runnable/app.ts +50 -14
  99. package/src/runnable/index.ts +20 -3
  100. package/src/runnable/primitives.test.ts +34 -0
  101. package/src/runnable/primitives.ts +116 -0
  102. package/src/runnable/runnable-types.ts +3 -0
  103. package/src/runnable/tool.ts +36 -5
  104. package/src/runnable/turn-hook.ts +8 -3
  105. package/src/slash-command.test.ts +69 -0
package/dist/cli.cjs CHANGED
@@ -103,6 +103,21 @@ function toSpecEnv(value) {
103
103
  }
104
104
  return Object.keys(env).length > 0 ? env : void 0;
105
105
  }
106
+ function validateRemoteUrl(name, raw) {
107
+ let parsed;
108
+ try {
109
+ parsed = new URL(raw);
110
+ } catch {
111
+ return `mcp server '${name}' remote url ${JSON.stringify(raw)} is invalid and was omitted`;
112
+ }
113
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
114
+ return `mcp server '${name}' remote url must use http or https and was omitted`;
115
+ }
116
+ if (parsed.username || parsed.password || parsed.hash) {
117
+ return `mcp server '${name}' remote url must not contain credentials or a fragment and was omitted`;
118
+ }
119
+ return void 0;
120
+ }
106
121
  function toSpecServer(name, decl) {
107
122
  const extras = {};
108
123
  const commandEnv = asString(decl.command_env);
@@ -120,6 +135,37 @@ function toSpecServer(name, decl) {
120
135
  note: `mcp server '${name}' is disabled in the native manifest and was omitted from ${AGENT_PLUGIN_MCP_FILE}`
121
136
  };
122
137
  }
138
+ const declaredType = asString(decl.type);
139
+ const remoteUrl = asString(decl.url);
140
+ const remoteType = declaredType === "sse" ? "sse" : declaredType === "streamable-http" || declaredType === "streamable_http" || declaredType === "http" || !declaredType && remoteUrl ? "streamable-http" : void 0;
141
+ if (remoteType) {
142
+ if (!remoteUrl) {
143
+ return {
144
+ extras,
145
+ note: `mcp server '${name}' has remote type '${remoteType}' but no url and was omitted`
146
+ };
147
+ }
148
+ const urlNote = validateRemoteUrl(name, remoteUrl);
149
+ if (urlNote) {
150
+ return { extras, note: urlNote };
151
+ }
152
+ const server2 = {
153
+ type: remoteType,
154
+ url: remoteUrl
155
+ };
156
+ const headers = toSpecEnv(decl.headers);
157
+ if (headers) {
158
+ server2.headers = headers;
159
+ }
160
+ const note = decl.auth ? `mcp server '${name}' uses Ryu OAuth metadata that is not portable; static headers were exported` : void 0;
161
+ return { server: server2, extras, note };
162
+ }
163
+ if (declaredType && declaredType !== "stdio") {
164
+ return {
165
+ extras,
166
+ note: `mcp server '${name}' has unsupported transport '${declaredType}' and was omitted`
167
+ };
168
+ }
123
169
  const command2 = asString(decl.command);
124
170
  if (!command2) {
125
171
  return {
@@ -313,9 +359,9 @@ function resolveGatewayUrl3() {
313
359
 
314
360
  // src/cli/dev.ts
315
361
  var RE_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/;
316
- async function probeGateway(baseUrl) {
362
+ async function probeGateway(baseUrl, fetchImpl = fetch) {
317
363
  try {
318
- await fetch(`${baseUrl}/health`, {
364
+ await fetchImpl(`${baseUrl}/health`, {
319
365
  method: "HEAD",
320
366
  signal: AbortSignal.timeout(3e3)
321
367
  });
@@ -630,8 +676,38 @@ var WidgetContributionSchema = import_zod.z.object({
630
676
  /** Default display mode (`inline` | `fullscreen` | `pip`). */
631
677
  default_display_mode: import_zod.z.string().default(DEFAULT_WIDGET_DISPLAY_MODE)
632
678
  });
679
+ var ChatWidgetTemplateSchema = import_zod.z.object({
680
+ id: import_zod.z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/),
681
+ title: import_zod.z.string().min(1),
682
+ description: import_zod.z.string().optional(),
683
+ triggers: import_zod.z.array(import_zod.z.string()).default([]),
684
+ examples: import_zod.z.array(import_zod.z.string()).default([]),
685
+ backing: import_zod.z.object({
686
+ tool_id: import_zod.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/).optional(),
687
+ view_id: import_zod.z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/).optional()
688
+ }),
689
+ display_mode: import_zod.z.string().min(1),
690
+ safe_action_ids: import_zod.z.array(import_zod.z.string().regex(/^[a-z0-9][a-z0-9._-]*$/)).default([]),
691
+ availability: import_zod.z.string().default("available")
692
+ }).superRefine((value, ctx) => {
693
+ const count = Number(Boolean(value.backing.tool_id)) + Number(Boolean(value.backing.view_id));
694
+ if (count !== 1 && value.availability === "available") {
695
+ ctx.addIssue({
696
+ code: "custom",
697
+ path: ["backing"],
698
+ message: "available templates need exactly one backing tool_id or view_id"
699
+ });
700
+ }
701
+ if (count > 1) {
702
+ ctx.addIssue({
703
+ code: "custom",
704
+ path: ["backing"],
705
+ message: "backing must declare at most one of tool_id or view_id"
706
+ });
707
+ }
708
+ });
633
709
  var ToolAppConfigSchema = import_zod.z.object({
634
- /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
710
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
635
711
  slug: import_zod.z.string().min(1),
636
712
  /** The tool description the model reads when choosing it. Carried here because a
637
713
  * packed app's manifest is the only channel (there is no `generated.rs`); Core's
@@ -650,6 +726,33 @@ var ToolAppConfigSchema = import_zod.z.object({
650
726
  /** Optional status label shown when the render tool finishes. */
651
727
  invoked: import_zod.z.string().optional()
652
728
  });
729
+ var SlashCommandOptionSchema = import_zod.z.object({
730
+ description: import_zod.z.string().optional(),
731
+ label: import_zod.z.string().min(1),
732
+ value: import_zod.z.string().min(1)
733
+ }).passthrough();
734
+ var SlashCommandCustomOptionSchema = import_zod.z.union([
735
+ import_zod.z.literal(true),
736
+ import_zod.z.object({
737
+ description: import_zod.z.string().optional(),
738
+ label: import_zod.z.string().min(1).optional()
739
+ }).passthrough()
740
+ ]);
741
+ var SlashCommandArgumentSchema = import_zod.z.object({
742
+ allow_custom: import_zod.z.boolean().optional(),
743
+ custom: SlashCommandCustomOptionSchema.optional(),
744
+ description: import_zod.z.string().optional(),
745
+ name: import_zod.z.string().min(1),
746
+ options: import_zod.z.array(SlashCommandOptionSchema).optional()
747
+ }).passthrough();
748
+ var SlashCommandContributionSchema = import_zod.z.object({
749
+ args: import_zod.z.array(SlashCommandArgumentSchema).optional(),
750
+ body: import_zod.z.string().optional(),
751
+ command: import_zod.z.string().min(1),
752
+ description: import_zod.z.string().optional(),
753
+ id: import_zod.z.string().optional(),
754
+ parameters: import_zod.z.array(SlashCommandArgumentSchema).optional()
755
+ }).passthrough();
653
756
  var ContributesSchema = import_zod.z.object({
654
757
  turn_hooks: import_zod.z.array(TurnHookContributionSchema).default([]),
655
758
  /** App events this plugin EMITS — the provider half of the hook system, whose
@@ -658,13 +761,20 @@ var ContributesSchema = import_zod.z.object({
658
761
  * signing, leaving an app that emits events nothing is allowed to subscribe to. */
659
762
  hook_events: import_zod.z.array(HookEventContributionSchema).default([]),
660
763
  composer_controls: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
764
+ /** Chat feature descriptors whose behavior is implemented by the host shell.
765
+ * Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
766
+ * authoring schema prevents `ryu pack` from silently deleting a plugin's chat
767
+ * feature declaration before signing. */
768
+ chat_features: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
661
769
  settings_tabs: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
662
- slash_commands: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
770
+ slash_commands: import_zod.z.array(SlashCommandContributionSchema).default([]),
663
771
  /** App widgets (Ryu Apps). Each binds a render tool id to its
664
772
  * `ui://widget/<slug>.html` template. Mirrors the Rust-side
665
773
  * `Contributes.widgets` field, without which the CLI's zod parse would strip
666
774
  * every widget an app authored here declares. */
667
775
  widgets: import_zod.z.array(WidgetContributionSchema).default([]),
776
+ /** Metadata-only chat widget templates. */
777
+ chat_widget_templates: import_zod.z.array(ChatWidgetTemplateSchema).optional(),
668
778
  /** App-registered sidebar sections (header + live list) and buttons (single nav
669
779
  * rows). Loosely typed here — the shell owns the spec vocabulary — matching how
670
780
  * `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
@@ -677,6 +787,13 @@ var ContributesSchema = import_zod.z.object({
677
787
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
678
788
  * strip the dock panel an app declares here. */
679
789
  dock_panels: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
790
+ /** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
791
+ * typed for the same reason as the surfaces above — the shell owns the
792
+ * `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
793
+ * without it the CLI's zod parse would strip every live activity an app
794
+ * declares here, so a packed bundle would ship a dock that silently stays
795
+ * empty. */
796
+ live_activities: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
680
797
  /** Deletable data categories the app owns — one "Delete all X" row in Settings
681
798
  * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
682
799
  * it the CLI's zod parse would strip the declaration before signing, and the
@@ -714,7 +831,13 @@ var ContributesSchema = import_zod.z.object({
714
831
  * bug: the styles' `.md` files are not carried by the bundle either, so there
715
832
  * would be no residue to notice — the plugin would install clean and contribute
716
833
  * nothing. */
717
- output_styles: import_zod.z.array(OutputStyleContributionSchema).default([])
834
+ output_styles: import_zod.z.array(OutputStyleContributionSchema).default([]),
835
+ /** Per-message actions contributed by an enabled plugin. Kept as loose records
836
+ * so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
837
+ message_actions: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
838
+ /** Buttons contributed to the floating text-selection toolbar. Kept as loose
839
+ * records so host-owned dispatch args survive `ryu pack` unchanged. */
840
+ selection_actions: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([])
718
841
  });
719
842
  var SetupStepSchema = import_zod.z.object({
720
843
  /** Card heading (e.g. the companion app name). */
@@ -795,6 +918,61 @@ var EnginesReqSchema = import_zod.z.object({
795
918
  /** Floor for the Next.js web app. */
796
919
  web: import_zod.z.string().optional()
797
920
  });
921
+ var McpServerAuthSchema = import_zod.z.object({
922
+ client_id: import_zod.z.string().min(1).optional(),
923
+ type: import_zod.z.literal("oauth")
924
+ }).strict();
925
+ var McpServerDeclSchema = import_zod.z.object({
926
+ args: import_zod.z.array(import_zod.z.string()).default([]),
927
+ auth: McpServerAuthSchema.optional(),
928
+ command: import_zod.z.string().optional(),
929
+ command_env: import_zod.z.string().optional(),
930
+ description: import_zod.z.string().optional(),
931
+ enabled: import_zod.z.boolean().default(true),
932
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}),
933
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}),
934
+ type: import_zod.z.enum(["stdio", "http", "streamable-http", "streamable_http", "sse"]).optional(),
935
+ url: import_zod.z.url().optional()
936
+ }).superRefine((server, context) => {
937
+ if (!(server.command || server.url)) {
938
+ context.addIssue({
939
+ code: "custom",
940
+ message: "an MCP server requires command or url"
941
+ });
942
+ }
943
+ if (!server.auth) {
944
+ return;
945
+ }
946
+ if (server.command || server.type === "stdio" || !server.url) {
947
+ context.addIssue({
948
+ code: "custom",
949
+ message: "OAuth is supported only for remote HTTP MCP servers"
950
+ });
951
+ return;
952
+ }
953
+ if (Object.keys(server.headers).some(
954
+ (name) => name.toLowerCase() === "authorization"
955
+ )) {
956
+ context.addIssue({
957
+ code: "custom",
958
+ message: "OAuth cannot be combined with a static Authorization header"
959
+ });
960
+ }
961
+ const url = new URL(server.url);
962
+ const loopback = url.hostname === "localhost" || url.hostname.startsWith("127.") || url.hostname === "[::1]" || url.hostname === "::1";
963
+ if (url.username || url.password || url.hash) {
964
+ context.addIssue({
965
+ code: "custom",
966
+ message: "OAuth MCP URLs cannot contain credentials or fragments"
967
+ });
968
+ }
969
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
970
+ context.addIssue({
971
+ code: "custom",
972
+ message: "OAuth MCP URLs must use HTTPS except on loopback"
973
+ });
974
+ }
975
+ });
798
976
  var PluginManifestSchema = import_zod.z.object({
799
977
  /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
800
978
  id: import_zod.z.string().min(1, "id is required"),
@@ -809,6 +987,9 @@ var PluginManifestSchema = import_zod.z.object({
809
987
  /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
810
988
  "version must be a valid semver string (e.g. 1.0.0)"
811
989
  ),
990
+ /** Core-owned release maturity metadata. The Rust contract validates the
991
+ * richer shape; the SDK authoring parser must preserve it for pack/publish. */
992
+ stability: import_zod.z.unknown().optional(),
812
993
  /**
813
994
  * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
814
995
  * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
@@ -826,6 +1007,26 @@ var PluginManifestSchema = import_zod.z.object({
826
1007
  * Declarations only — grant enforcement is the Gateway's responsibility.
827
1008
  */
828
1009
  permission_grants: import_zod.z.array(import_zod.z.string()).default([]),
1010
+ /** Deny-by-default sandbox permissions. Core remains authoritative; this
1011
+ * schema mirrors the fields so `ryu pack` cannot strip them. */
1012
+ permissions: import_zod.z.object({
1013
+ fs: import_zod.z.object({
1014
+ read: import_zod.z.array(import_zod.z.string()).default([]),
1015
+ write: import_zod.z.array(import_zod.z.string()).default([])
1016
+ }).optional(),
1017
+ child_process: import_zod.z.boolean().optional(),
1018
+ run: import_zod.z.array(import_zod.z.string()).default([]),
1019
+ network: import_zod.z.union([import_zod.z.boolean(), import_zod.z.array(import_zod.z.string())]).optional(),
1020
+ tool: import_zod.z.array(import_zod.z.string()).default([])
1021
+ }).optional(),
1022
+ /** Core-owned permission presentation levels. Preserved verbatim here and
1023
+ * validated by Core's authoritative manifest contract. */
1024
+ permission_levels: import_zod.z.unknown().optional(),
1025
+ /** Remote or stdio MCP servers registered by this plugin. */
1026
+ mcp_servers: import_zod.z.record(import_zod.z.string(), McpServerDeclSchema).optional(),
1027
+ /** Core-owned sidecar declarations. Their full process/HTTP schema stays in
1028
+ * the Rust contract; this authoring layer must never strip them. */
1029
+ sidecars: import_zod.z.unknown().optional(),
829
1030
  /**
830
1031
  * Optional Companion surface (an in-desktop overlay or sidebar panel).
831
1032
  * Absent when the plugin has no Companion surface.
@@ -861,6 +1062,9 @@ var PluginManifestSchema = import_zod.z.object({
861
1062
  * an unsupported-target plugin stays installable and inspectable.
862
1063
  */
863
1064
  targets: import_zod.z.array(SurfaceSchema).default([]),
1065
+ /** Rich per-surface declarations (`support`, UI, contributed commands, …).
1066
+ * Core owns and validates the nested vocabulary. */
1067
+ surfaces: import_zod.z.unknown().optional(),
864
1068
  /**
865
1069
  * Host version floors — the semver requirement each surface must satisfy for
866
1070
  * this plugin to install. Mirrors Core's `EnginesReq`
@@ -919,10 +1123,16 @@ var PluginManifestSchema = import_zod.z.object({
919
1123
  url: import_zod.z.string().optional()
920
1124
  })
921
1125
  ]).optional(),
1126
+ /** Public source repository URL (Claude/Codex `repository`). */
1127
+ repository: import_zod.z.string().url().optional(),
1128
+ /** True when the provider operates outside the local Ryu runtime. */
1129
+ external: import_zod.z.boolean().optional(),
922
1130
  /** Project/marketing homepage — maps to the listing `website` (Claude field). */
923
1131
  homepage: import_zod.z.string().optional(),
924
1132
  /** Free-text search keywords (Claude field). */
925
1133
  keywords: import_zod.z.array(import_zod.z.string()).optional(),
1134
+ /** Stable Marketplace filter labels (Ryu extension). */
1135
+ tags: import_zod.z.array(import_zod.z.string()).optional(),
926
1136
  /** Taxonomy category beyond the runnable kinds (Claude field). */
927
1137
  category: import_zod.z.string().optional(),
928
1138
  /** SPDX-ish license identifier (Claude field). */
@@ -936,6 +1146,8 @@ var PluginManifestSchema = import_zod.z.object({
936
1146
  * from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
937
1147
  */
938
1148
  icon: import_zod.z.string().optional(),
1149
+ /** Optional detail-page hero banner metadata forwarded to the marketplace. */
1150
+ banner: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
939
1151
  /**
940
1152
  * Dithered-gradient background for the card's icon square (Ryu extension),
941
1153
  * mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
@@ -967,6 +1179,22 @@ var PluginManifestSchema = import_zod.z.object({
967
1179
  * array of steps guiding the user through post-install configuration.
968
1180
  */
969
1181
  setup: import_zod.z.union([SetupStepSchema, import_zod.z.array(SetupStepSchema)]).optional()
1182
+ }).passthrough().superRefine((manifest, context) => {
1183
+ const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
1184
+ (server) => server.auth?.type === "oauth"
1185
+ );
1186
+ if (!hasOAuthServer) {
1187
+ return;
1188
+ }
1189
+ for (const grant of ["mcp:server", "identity.read"]) {
1190
+ if (!manifest.permission_grants.includes(grant)) {
1191
+ context.addIssue({
1192
+ code: "custom",
1193
+ message: `OAuth MCP servers require the ${grant} permission grant`,
1194
+ path: ["permission_grants"]
1195
+ });
1196
+ }
1197
+ }
970
1198
  });
971
1199
 
972
1200
  // src/cli.ts
@@ -980,7 +1208,9 @@ function printUsage() {
980
1208
  "",
981
1209
  "Usage:",
982
1210
  " bunx ryu pack <dir> Validate and bundle a manifest.json Plugin",
983
- " bunx ryu publish <dir> Validate and publish a manifest.json Plugin to the Ryu Marketplace",
1211
+ " bunx ryu github-publish <url>",
1212
+ " Submit a GitHub repository/package URL to the Marketplace",
1213
+ " bunx ryu publish <dir> Deprecated server-artifact publishing (migration only)",
984
1214
  " bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
985
1215
  " bunx ryu agent-plugin <dir>",
986
1216
  " Emit the Agent Plugins v1 interop pair (plugin.json + mcp.json)",
@@ -989,7 +1219,7 @@ function printUsage() {
989
1219
  );
990
1220
  }
991
1221
  function exitError(message) {
992
- process.stderr.write(`error: ${message}
1222
+ (0, import_node_fs.writeSync)(process.stderr.fd, `error: ${message}
993
1223
  `);
994
1224
  process.exit(1);
995
1225
  }
@@ -999,16 +1229,20 @@ var MANIFEST_FILE_NAMES = [
999
1229
  "ryu.json"
1000
1230
  ];
1001
1231
  function resolveNativeManifestPath(dir) {
1002
- return MANIFEST_FILE_NAMES.map((name) => (0, import_node_path.join)(dir, name)).find((candidate) => {
1003
- if (!(0, import_node_fs.existsSync)(candidate)) {
1004
- return false;
1005
- }
1006
- try {
1007
- return !isAgentPluginManifest(JSON.parse((0, import_node_fs.readFileSync)(candidate, "utf8")));
1008
- } catch {
1009
- return true;
1232
+ return MANIFEST_FILE_NAMES.map((name) => (0, import_node_path.join)(dir, name)).find(
1233
+ (candidate) => {
1234
+ if (!(0, import_node_fs.existsSync)(candidate)) {
1235
+ return false;
1236
+ }
1237
+ try {
1238
+ return !isAgentPluginManifest(
1239
+ JSON.parse((0, import_node_fs.readFileSync)(candidate, "utf8"))
1240
+ );
1241
+ } catch {
1242
+ return true;
1243
+ }
1010
1244
  }
1011
- });
1245
+ );
1012
1246
  }
1013
1247
  function loadManifest(dir) {
1014
1248
  const manifestPath = resolveNativeManifestPath(dir);
@@ -1038,13 +1272,30 @@ function loadManifest(dir) {
1038
1272
  }
1039
1273
  var CODE_FILE_DIRS = ["hooks", "adapters"];
1040
1274
  var CODE_FILE_PATH = /^(hooks|adapters)\/[A-Za-z0-9_][A-Za-z0-9._-]*\.m?js$/;
1275
+ function containedPackageFile(dir, rel, label) {
1276
+ let realRoot;
1277
+ let realTarget;
1278
+ try {
1279
+ realRoot = (0, import_node_fs.realpathSync)(dir);
1280
+ realTarget = (0, import_node_fs.realpathSync)((0, import_node_path.join)(dir, rel));
1281
+ } catch (error) {
1282
+ exitError(`${label}: could not resolve '${rel}': ${String(error)}`);
1283
+ }
1284
+ const fromRoot = (0, import_node_path.relative)(realRoot, realTarget);
1285
+ if (fromRoot === ".." || fromRoot.startsWith(`..${import_node_path.sep}`) || (0, import_node_path.isAbsolute)(fromRoot)) {
1286
+ exitError(
1287
+ `${label}: '${rel}' resolves outside the plugin package (${realTarget})`
1288
+ );
1289
+ }
1290
+ return realTarget;
1291
+ }
1041
1292
  function readCodeFile(dir, rel, label) {
1042
1293
  if (!CODE_FILE_PATH.test(rel)) {
1043
1294
  exitError(
1044
1295
  `${label}: code_file '${rel}' must be exactly '<${CODE_FILE_DIRS.join("|")}>/<name>.js' with no traversal`
1045
1296
  );
1046
1297
  }
1047
- const path = (0, import_node_path.join)(dir, rel);
1298
+ const path = containedPackageFile(dir, rel, label);
1048
1299
  let body;
1049
1300
  try {
1050
1301
  body = (0, import_node_fs.readFileSync)(path, "utf8");
@@ -1094,7 +1345,7 @@ function inlineOutputStyleFiles(manifest, dir) {
1094
1345
  }
1095
1346
  let body;
1096
1347
  try {
1097
- body = (0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, rel), "utf8");
1348
+ body = (0, import_node_fs.readFileSync)(containedPackageFile(dir, rel, label), "utf8");
1098
1349
  } catch (err) {
1099
1350
  exitError(`${label}: could not read file '${rel}': ${String(err)}`);
1100
1351
  }
@@ -1140,6 +1391,12 @@ function resolveUiFormat(manifest) {
1140
1391
  return "html";
1141
1392
  }
1142
1393
  }
1394
+ for (const widget of manifest.contributes?.widgets ?? []) {
1395
+ const entry = widget.ui_entry;
1396
+ if (typeof entry === "string" && entry.trim().toLowerCase().endsWith(".html")) {
1397
+ return "html";
1398
+ }
1399
+ }
1143
1400
  return "js";
1144
1401
  }
1145
1402
  function readUiEntryHtml(dir, uiEntry) {
@@ -1271,8 +1528,16 @@ async function commandPublish(rawDir) {
1271
1528
  ...manifest.description ? { description: manifest.description } : {},
1272
1529
  ...manifest.tagline ? { tagline: manifest.tagline } : {},
1273
1530
  ...developer ? { developer } : {},
1531
+ ...manifest.author ? { author: manifest.author } : {},
1532
+ ...manifest.repository ? { repository: manifest.repository } : {},
1533
+ ...manifest.external ? { external: true } : {},
1274
1534
  ...manifest.category ? { category: manifest.category } : {},
1535
+ ...manifest.license ? { license: manifest.license } : {},
1536
+ ...manifest.keywords?.length ? { keywords: manifest.keywords } : {},
1275
1537
  ...manifest.iconUrl ? { iconUrl: manifest.iconUrl } : {},
1538
+ ...manifest.icon ? { icon: manifest.icon } : {},
1539
+ ...manifest.iconDither ? { iconDither: manifest.iconDither } : {},
1540
+ ...manifest.banner ? { banner: manifest.banner } : {},
1276
1541
  ...manifest.screenshots?.length ? { screenshots: manifest.screenshots } : {},
1277
1542
  ...manifest.homepage ? { website: manifest.homepage } : {},
1278
1543
  ...manifest.privacyPolicyUrl ? { privacyPolicyUrl: manifest.privacyPolicyUrl } : {},
@@ -1328,6 +1593,55 @@ ${text}
1328
1593
  `
1329
1594
  );
1330
1595
  }
1596
+ async function commandGithubPublish(repositoryUrl) {
1597
+ const input = repositoryUrl.trim();
1598
+ if (!input) {
1599
+ exitError(
1600
+ "github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
1601
+ );
1602
+ }
1603
+ const token = authToken();
1604
+ const url = `${publishBaseUrl()}/api/marketplace/github/publish`;
1605
+ const proof = process.env.RYU_GITHUB_INSTALLATION_PROOF?.trim();
1606
+ const body = {
1607
+ url: input,
1608
+ ...proof ? { installationProof: proof } : {}
1609
+ };
1610
+ let response;
1611
+ try {
1612
+ response = await fetch(url, {
1613
+ method: "POST",
1614
+ headers: {
1615
+ authorization: `Bearer ${token}`,
1616
+ "content-type": "application/json"
1617
+ },
1618
+ body: JSON.stringify(body)
1619
+ });
1620
+ } catch (error) {
1621
+ exitError(`could not reach ${url}: ${String(error)}`);
1622
+ }
1623
+ const text = await response.text();
1624
+ if (!response.ok) {
1625
+ try {
1626
+ const error = JSON.parse(text);
1627
+ if (error.installationUrl) {
1628
+ exitError(
1629
+ `${error.error ?? "GitHub App installation required"}. Open ${error.installationUrl}, then set RYU_GITHUB_INSTALLATION_PROOF to the callback proof and retry.`
1630
+ );
1631
+ }
1632
+ exitError(
1633
+ `github-publish failed (${response.status}): ${error.error ?? text}`
1634
+ );
1635
+ } catch {
1636
+ exitError(`github-publish failed (${response.status}): ${text}`);
1637
+ }
1638
+ }
1639
+ process.stdout.write(
1640
+ `GitHub-backed listing submitted for moderation:
1641
+ ${text}
1642
+ `
1643
+ );
1644
+ }
1331
1645
  var [, , command, ...args] = process.argv;
1332
1646
  if (!command) {
1333
1647
  printUsage();
@@ -1346,9 +1660,24 @@ if (command === "pack") {
1346
1660
  if (!dir) {
1347
1661
  exitError("publish requires a directory argument: bunx ryu publish <dir>");
1348
1662
  }
1663
+ if (process.env.RYU_ENABLE_LEGACY_MARKETPLACE_WRITES !== "true") {
1664
+ exitError(
1665
+ "ryu publish is deprecated because marketplace package contents now live in GitHub Releases; use bunx ryu github-publish <repository-or-package-url>"
1666
+ );
1667
+ }
1349
1668
  commandPublish(dir).catch((err) => {
1350
1669
  exitError(String(err));
1351
1670
  });
1671
+ } else if (command === "github-publish") {
1672
+ const repositoryUrl = args[0];
1673
+ if (!repositoryUrl) {
1674
+ exitError(
1675
+ "github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
1676
+ );
1677
+ }
1678
+ commandGithubPublish(repositoryUrl).catch((err) => {
1679
+ exitError(String(err));
1680
+ });
1352
1681
  } else if (command === "agent-plugin") {
1353
1682
  const dir = args[0];
1354
1683
  if (!dir) {