@ryuhq/sdk 0.1.14 → 0.2.0

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 (90) hide show
  1. package/README.md +19 -6
  2. package/dist/agent-plugin.cjs +46 -0
  3. package/dist/agent-plugin.d.cts +42 -32
  4. package/dist/agent-plugin.d.ts +42 -32
  5. package/dist/agent-plugin.js +1 -1
  6. package/dist/agent.cjs +9 -0
  7. package/dist/agent.d.cts +377 -1
  8. package/dist/agent.d.ts +377 -1
  9. package/dist/agent.js +4 -2
  10. package/dist/app-Bkw7LlCK.d.ts +129 -0
  11. package/dist/app-DNaGmLVf.d.cts +129 -0
  12. package/dist/builder.cjs +1095 -0
  13. package/dist/builder.d.cts +212 -0
  14. package/dist/builder.d.ts +212 -0
  15. package/dist/builder.js +28 -0
  16. package/dist/chunk-A3RGEPDG.js +250 -0
  17. package/dist/chunk-FZSFZOIN.js +200 -0
  18. package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
  19. package/dist/{chunk-AO2KJRDD.js → chunk-IEUQ3CDG.js} +125 -2
  20. package/dist/chunk-IKEDLLFY.js +19 -0
  21. package/dist/chunk-IOLP5FFE.js +354 -0
  22. package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
  23. package/dist/chunk-T5676WL2.js +240 -0
  24. package/dist/chunk-TLDPEGC7.js +21 -0
  25. package/dist/chunk-TXSHHZF2.js +0 -0
  26. package/dist/{chunk-MTUBUPIV.js → chunk-ULSVL7EC.js} +8 -227
  27. package/dist/chunk-VLIRNNAE.js +154 -0
  28. package/dist/chunk-W3KPP4WN.js +135 -0
  29. package/dist/cli.cjs +258 -14
  30. package/dist/cli.js +96 -16
  31. package/dist/client-D5U6ssPc.d.cts +84 -0
  32. package/dist/client-D5U6ssPc.d.ts +84 -0
  33. package/dist/index.cjs +519 -5
  34. package/dist/index.d.cts +16 -632
  35. package/dist/index.d.ts +16 -632
  36. package/dist/index.js +63 -695
  37. package/dist/manifest.cjs +129 -3
  38. package/dist/manifest.d.cts +105 -2
  39. package/dist/manifest.d.ts +105 -2
  40. package/dist/manifest.js +7 -1
  41. package/dist/mcp/client.cjs +180 -0
  42. package/dist/mcp/client.d.cts +49 -0
  43. package/dist/mcp/client.d.ts +49 -0
  44. package/dist/mcp/client.js +10 -0
  45. package/dist/mcp/server.cjs +370 -0
  46. package/dist/mcp/server.d.cts +126 -0
  47. package/dist/mcp/server.d.ts +126 -0
  48. package/dist/mcp/server.js +9 -0
  49. package/dist/mcp.cjs +376 -0
  50. package/dist/mcp.d.cts +2 -0
  51. package/dist/mcp.d.ts +2 -0
  52. package/dist/mcp.js +17 -0
  53. package/dist/model.cjs +141 -0
  54. package/dist/model.d.cts +33 -0
  55. package/dist/model.d.ts +33 -0
  56. package/dist/model.js +18 -0
  57. package/dist/plugin.cjs +46 -0
  58. package/dist/plugin.d.cts +215 -0
  59. package/dist/plugin.d.ts +215 -0
  60. package/dist/plugin.js +8 -0
  61. package/dist/runnable.cjs +1230 -0
  62. package/dist/runnable.d.cts +271 -0
  63. package/dist/runnable.d.ts +271 -0
  64. package/dist/runnable.js +28 -0
  65. package/dist/{index-B6SkaAjJ.d.ts → tool-DSx2bFx8.d.ts} +35 -458
  66. package/dist/{index-BvAB5eMk.d.cts → tool-u-VR0fLF.d.cts} +35 -458
  67. package/package.json +38 -2
  68. package/src/agent/loop.test.ts +4 -4
  69. package/src/agent/tools.ts +3 -3
  70. package/src/agent-plugin.test.ts +58 -0
  71. package/src/agent-plugin.ts +108 -35
  72. package/src/cli/dev.test.ts +26 -47
  73. package/src/cli/dev.ts +10 -2
  74. package/src/cli.ts +98 -15
  75. package/src/exports-lockstep.test.ts +92 -0
  76. package/src/generated/plugin-manifest.ts +243 -27
  77. package/src/index.ts +35 -0
  78. package/src/manifest-schema.test.ts +30 -1
  79. package/src/manifest.fixtures.test.ts +13 -3
  80. package/src/manifest.test.ts +96 -10
  81. package/src/manifest.ts +334 -187
  82. package/src/mcp/index.ts +18 -0
  83. package/src/model/index.ts +22 -0
  84. package/src/plugin/ryu-plugin.ts +82 -0
  85. package/src/runnable/app.test.ts +2 -0
  86. package/src/runnable/app.ts +5 -2
  87. package/src/runnable/index.ts +2 -0
  88. package/src/runnable/primitives.ts +57 -0
  89. package/src/runnable/tool.ts +1 -1
  90. package/src/runnable/turn-hook.ts +4 -1
package/src/manifest.ts CHANGED
@@ -344,6 +344,35 @@ export const WidgetContributionSchema = z.object({
344
344
 
345
345
  export type WidgetContribution = z.infer<typeof WidgetContributionSchema>;
346
346
 
347
+ /** Metadata-only chat affordance. The host owns rendering and dispatch; the
348
+ * manifest carries identifiers and copy only. */
349
+ export const ChatWidgetTemplateSchema = z.object({
350
+ id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/),
351
+ title: z.string().min(1),
352
+ description: z.string().optional(),
353
+ triggers: z.array(z.string()).default([]),
354
+ examples: z.array(z.string()).default([]),
355
+ backing: z
356
+ .object({
357
+ tool_id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/).optional(),
358
+ view_id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/).optional(),
359
+ }),
360
+ display_mode: z.string().min(1),
361
+ safe_action_ids: z.array(z.string().regex(/^[a-z0-9][a-z0-9._-]*$/)).default([]),
362
+ availability: z.string().default("available"),
363
+ })
364
+ .superRefine((value, ctx) => {
365
+ const count = Number(Boolean(value.backing.tool_id)) + Number(Boolean(value.backing.view_id));
366
+ if (count !== 1 && value.availability === "available") {
367
+ ctx.addIssue({ code: "custom", path: ["backing"], message: "available templates need exactly one backing tool_id or view_id" });
368
+ }
369
+ if (count > 1) {
370
+ ctx.addIssue({ code: "custom", path: ["backing"], message: "backing must declare at most one of tool_id or view_id" });
371
+ }
372
+ });
373
+
374
+ export type ChatWidgetTemplate = z.infer<typeof ChatWidgetTemplateSchema>;
375
+
347
376
  // ── ToolAppConfig (Ryu Apps per-tool config) ─────────────────────────────────
348
377
 
349
378
  /**
@@ -355,7 +384,7 @@ export type WidgetContribution = z.infer<typeof WidgetContributionSchema>;
355
384
  * derives `outputTemplate` / `toolInvocation` / `widgetAccessible`.
356
385
  */
357
386
  export const ToolAppConfigSchema = z.object({
358
- /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
387
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
359
388
  slug: z.string().min(1),
360
389
  /** The tool description the model reads when choosing it. Carried here because a
361
390
  * packed app's manifest is the only channel (there is no `generated.rs`); Core's
@@ -380,7 +409,7 @@ export type ToolAppConfig = z.infer<typeof ToolAppConfigSchema>;
380
409
  /**
381
410
  * The `contributes` block. Mirrors `Contributes` in
382
411
  * `apps/core/src/plugin_manifest/mod.rs`. The declarative UI surfaces
383
- * (`composer_controls` / `settings_tabs` / `slash_commands`) are passed verbatim
412
+ * (`composer_controls` / `chat_features` / `settings_tabs` / `slash_commands`) are passed verbatim
384
413
  * to the desktop renderer, so they are typed loosely here (records).
385
414
  */
386
415
  export const ContributesSchema = z.object({
@@ -391,6 +420,11 @@ export const ContributesSchema = z.object({
391
420
  * signing, leaving an app that emits events nothing is allowed to subscribe to. */
392
421
  hook_events: z.array(HookEventContributionSchema).default([]),
393
422
  composer_controls: z.array(z.record(z.string(), z.unknown())).default([]),
423
+ /** Chat feature descriptors whose behavior is implemented by the host shell.
424
+ * Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
425
+ * authoring schema prevents `ryu pack` from silently deleting a plugin's chat
426
+ * feature declaration before signing. */
427
+ chat_features: z.array(z.record(z.string(), z.unknown())).default([]),
394
428
  settings_tabs: z.array(z.record(z.string(), z.unknown())).default([]),
395
429
  slash_commands: z.array(z.record(z.string(), z.unknown())).default([]),
396
430
  /** App widgets (Ryu Apps). Each binds a render tool id to its
@@ -398,6 +432,8 @@ export const ContributesSchema = z.object({
398
432
  * `Contributes.widgets` field, without which the CLI's zod parse would strip
399
433
  * every widget an app authored here declares. */
400
434
  widgets: z.array(WidgetContributionSchema).default([]),
435
+ /** Metadata-only chat widget templates. */
436
+ chat_widget_templates: z.array(ChatWidgetTemplateSchema).optional(),
401
437
  /** App-registered sidebar sections (header + live list) and buttons (single nav
402
438
  * rows). Loosely typed here — the shell owns the spec vocabulary — matching how
403
439
  * `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
@@ -410,6 +446,13 @@ export const ContributesSchema = z.object({
410
446
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
411
447
  * strip the dock panel an app declares here. */
412
448
  dock_panels: z.array(z.record(z.string(), z.unknown())).default([]),
449
+ /** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
450
+ * typed for the same reason as the surfaces above — the shell owns the
451
+ * `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
452
+ * without it the CLI's zod parse would strip every live activity an app
453
+ * declares here, so a packed bundle would ship a dock that silently stays
454
+ * empty. */
455
+ live_activities: z.array(z.record(z.string(), z.unknown())).default([]),
413
456
  /** Deletable data categories the app owns — one "Delete all X" row in Settings
414
457
  * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
415
458
  * it the CLI's zod parse would strip the declaration before signing, and the
@@ -450,6 +493,9 @@ export const ContributesSchema = z.object({
450
493
  * would be no residue to notice — the plugin would install clean and contribute
451
494
  * nothing. */
452
495
  output_styles: z.array(OutputStyleContributionSchema).default([]),
496
+ /** Per-message actions contributed by an enabled plugin. Kept as loose records
497
+ * so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
498
+ message_actions: z.array(z.record(z.string(), z.unknown())).default([]),
453
499
  });
454
500
 
455
501
  export type Contributes = z.infer<typeof ContributesSchema>;
@@ -615,6 +661,78 @@ export const EnginesReqSchema = z.object({
615
661
 
616
662
  export type EnginesReq = z.infer<typeof EnginesReqSchema>;
617
663
 
664
+ export const McpServerAuthSchema = z
665
+ .object({
666
+ client_id: z.string().min(1).optional(),
667
+ type: z.literal("oauth"),
668
+ })
669
+ .strict();
670
+
671
+ export const McpServerDeclSchema = z
672
+ .object({
673
+ args: z.array(z.string()).default([]),
674
+ auth: McpServerAuthSchema.optional(),
675
+ command: z.string().optional(),
676
+ command_env: z.string().optional(),
677
+ description: z.string().optional(),
678
+ enabled: z.boolean().default(true),
679
+ env: z.record(z.string(), z.string()).default({}),
680
+ headers: z.record(z.string(), z.string()).default({}),
681
+ type: z
682
+ .enum(["stdio", "http", "streamable-http", "streamable_http", "sse"])
683
+ .optional(),
684
+ url: z.url().optional(),
685
+ })
686
+ .superRefine((server, context) => {
687
+ if (!(server.command || server.url)) {
688
+ context.addIssue({
689
+ code: "custom",
690
+ message: "an MCP server requires command or url",
691
+ });
692
+ }
693
+ if (!server.auth) {
694
+ return;
695
+ }
696
+ if (server.command || server.type === "stdio" || !server.url) {
697
+ context.addIssue({
698
+ code: "custom",
699
+ message: "OAuth is supported only for remote HTTP MCP servers",
700
+ });
701
+ return;
702
+ }
703
+ if (
704
+ Object.keys(server.headers).some(
705
+ (name) => name.toLowerCase() === "authorization"
706
+ )
707
+ ) {
708
+ context.addIssue({
709
+ code: "custom",
710
+ message: "OAuth cannot be combined with a static Authorization header",
711
+ });
712
+ }
713
+ const url = new URL(server.url);
714
+ const loopback =
715
+ url.hostname === "localhost" ||
716
+ url.hostname.startsWith("127.") ||
717
+ url.hostname === "[::1]" ||
718
+ url.hostname === "::1";
719
+ if (url.username || url.password || url.hash) {
720
+ context.addIssue({
721
+ code: "custom",
722
+ message: "OAuth MCP URLs cannot contain credentials or fragments",
723
+ });
724
+ }
725
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
726
+ context.addIssue({
727
+ code: "custom",
728
+ message: "OAuth MCP URLs must use HTTPS except on loopback",
729
+ });
730
+ }
731
+ });
732
+
733
+ export type McpServerAuth = z.infer<typeof McpServerAuthSchema>;
734
+ export type McpServerDecl = z.infer<typeof McpServerDeclSchema>;
735
+
618
736
  // ── PluginManifest ───────────────────────────────────────────────────────────
619
737
 
620
738
  /**
@@ -627,203 +745,232 @@ export type EnginesReq = z.infer<typeof EnginesReqSchema>;
627
745
  * - `runnables` may be empty for a "surface-only" plugin, but each entry must be
628
746
  * a valid `RunnableMeta`
629
747
  */
630
- export const PluginManifestSchema = z.object({
631
- /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
632
- id: z.string().min(1, "id is required"),
748
+ export const PluginManifestSchema = z
749
+ .object({
750
+ /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
751
+ id: z.string().min(1, "id is required"),
633
752
 
634
- /** Human-readable display name shown in the plugin store / launcher. */
635
- name: z.string().min(1, "name is required"),
753
+ /** Human-readable display name shown in the plugin store / launcher. */
754
+ name: z.string().min(1, "name is required"),
636
755
 
637
- /**
638
- * Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
639
- * whose version is not valid semver; the regex here enforces the same rule at
640
- * SDK-build time.
641
- */
642
- version: z
643
- .string()
644
- .regex(
645
- /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
646
- "version must be a valid semver string (e.g. 1.0.0)"
647
- ),
756
+ /**
757
+ * Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
758
+ * whose version is not valid semver; the regex here enforces the same rule at
759
+ * SDK-build time.
760
+ */
761
+ version: z
762
+ .string()
763
+ .regex(
764
+ /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
765
+ "version must be a valid semver string (e.g. 1.0.0)"
766
+ ),
648
767
 
649
- /**
650
- * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
651
- * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
652
- * write it here BEFORE the manifest is signed, so the hash rides INSIDE the
653
- * Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
654
- * Core's install path recomputes the hash over the fetched code and rejects a
655
- * mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
656
- * Mirrors Core's `PluginManifest.ui_code_sha256`.
657
- */
658
- ui_code_sha256: z.string().nullish(),
768
+ /**
769
+ * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
770
+ * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
771
+ * write it here BEFORE the manifest is signed, so the hash rides INSIDE the
772
+ * Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
773
+ * Core's install path recomputes the hash over the fetched code and rejects a
774
+ * mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
775
+ * Mirrors Core's `PluginManifest.ui_code_sha256`.
776
+ */
777
+ ui_code_sha256: z.string().nullish(),
659
778
 
660
- /** The Runnables this plugin bundles. */
661
- runnables: z.array(RunnableMetaSchema).default([]),
779
+ /** The Runnables this plugin bundles. */
780
+ runnables: z.array(RunnableMetaSchema).default([]),
662
781
 
663
- /**
664
- * Permission grants this plugin declares it needs (e.g. `"mcp:web_search"`).
665
- * Declarations only — grant enforcement is the Gateway's responsibility.
666
- */
667
- permission_grants: z.array(z.string()).default([]),
782
+ /**
783
+ * Permission grants this plugin declares it needs (e.g. `"mcp:web_search"`).
784
+ * Declarations only — grant enforcement is the Gateway's responsibility.
785
+ */
786
+ permission_grants: z.array(z.string()).default([]),
668
787
 
669
- /**
670
- * Optional Companion surface (an in-desktop overlay or sidebar panel).
671
- * Absent when the plugin has no Companion surface.
672
- */
673
- companion: CompanionSurfaceSchema.optional(),
788
+ /** Remote or stdio MCP servers registered by this plugin. */
789
+ mcp_servers: z.record(z.string(), McpServerDeclSchema).optional(),
674
790
 
675
- /**
676
- * VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
677
- * `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
678
- * enabled flag, so `["*"]` is the usual value.
679
- */
680
- activation_events: z.array(z.string()).default([]),
791
+ /**
792
+ * Optional Companion surface (an in-desktop overlay or sidebar panel).
793
+ * Absent when the plugin has no Companion surface.
794
+ */
795
+ companion: CompanionSurfaceSchema.optional(),
681
796
 
682
- /**
683
- * Contribution points: server-side turn hooks + declarative UI widgets.
684
- * Absent for a plugin that contributes nothing here.
685
- */
686
- contributes: ContributesSchema.optional(),
797
+ /**
798
+ * VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
799
+ * `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
800
+ * enabled flag, so `["*"]` is the usual value.
801
+ */
802
+ activation_events: z.array(z.string()).default([]),
687
803
 
688
- /**
689
- * **Plugin-to-plugin dependencies** the other plugins this one needs. Core
690
- * resolves them into a topological enable order (dependencies enable first;
691
- * disabling one is refused while an enabled dependent needs it).
692
- *
693
- * Absent = **no dependencies**, the backward-compatible default. Kept
694
- * `.optional()` (never defaulted) so a manifest that declares none serialises
695
- * with no `requires` key at all, exactly like Core's
696
- * `#[serde(skip_serializing_if = "Option::is_none")]`.
697
- */
698
- requires: RequiresSchema.optional(),
804
+ /**
805
+ * Contribution points: server-side turn hooks + declarative UI widgets.
806
+ * Absent for a plugin that contributes nothing here.
807
+ */
808
+ contributes: ContributesSchema.optional(),
699
809
 
700
- /**
701
- * Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
702
- * surface** the backward-compatible default, which must never be read as
703
- * "runs nowhere". Core filters only when the list is explicitly non-empty, and
704
- * only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
705
- * an unsupported-target plugin stays installable and inspectable.
706
- */
707
- targets: z.array(SurfaceSchema).default([]),
810
+ /**
811
+ * **Plugin-to-plugin dependencies** the other plugins this one needs. Core
812
+ * resolves them into a topological enable order (dependencies enable first;
813
+ * disabling one is refused while an enabled dependent needs it).
814
+ *
815
+ * Absent = **no dependencies**, the backward-compatible default. Kept
816
+ * `.optional()` (never defaulted) so a manifest that declares none serialises
817
+ * with no `requires` key at all, exactly like Core's
818
+ * `#[serde(skip_serializing_if = "Option::is_none")]`.
819
+ */
820
+ requires: RequiresSchema.optional(),
708
821
 
709
- /**
710
- * Host version floors the semver requirement each surface must satisfy for
711
- * this plugin to install. Mirrors Core's `EnginesReq`
712
- * (`crates/core/kernel-contracts/src/manifest.rs`).
713
- *
714
- * `ryu` is the **Core** floor and the only required key (it is the legacy
715
- * spelling; every manifest in the wild carries just that one). The rest are
716
- * optional per-surface floors.
717
- *
718
- * REGRESSION THIS FIXES: `engines` was absent from this schema entirely, and
719
- * zod strips unlisted keys — so `ryu pack` silently dropped the whole block
720
- * from every bundle it produced. A plugin could declare a Core floor, publish,
721
- * and ship a bundle that declared none. Any new host floor MUST be added here
722
- * as well as in the Rust contract, or it does not survive packing.
723
- */
724
- engines: EnginesReqSchema.optional(),
822
+ /**
823
+ * Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
824
+ * surface** the backward-compatible default, which must never be read as
825
+ * "runs nowhere". Core filters only when the list is explicitly non-empty, and
826
+ * only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
827
+ * an unsupported-target plugin stays installable and inspectable.
828
+ */
829
+ targets: z.array(SurfaceSchema).default([]),
725
830
 
726
- /**
727
- * Optional per-item AFFILIATE terms: the commission paid to a referrer when a
728
- * referred user buys this (paid) item. `value` is basis points for `percent`
729
- * (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
730
- * falls back to the seller org owner's default affiliate terms. This is the
731
- * authoring surface for the marketplace publish body's `affiliate` field (the
732
- * server re-validates it); it only takes effect on a paid item.
733
- */
734
- affiliate: z
735
- .object({
736
- enabled: z.boolean().default(false),
737
- rule: z
738
- .object({
739
- type: z.enum(["percent", "flat"]),
740
- value: z.number().nonnegative(),
741
- recurring: z.boolean().default(false),
742
- durationMonths: z.number().int().positive().nullish(),
743
- fundedBy: z.enum(["platform", "seller"]).default("platform"),
744
- })
745
- .optional(),
746
- })
747
- .optional(),
748
-
749
- // ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
750
- // Optional store-listing fields a plugin author declares so the marketplace
751
- // detail dialog renders a richer App-Store-style preview. Field names align
752
- // with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
753
- // one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
754
- // rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
755
- // body (not inside the signed manifest blob) so the control plane stores them.
756
- // All optional + additive: a manifest omitting them still validates.
757
-
758
- /** Longer plain/markdown description shown in the detail dialog. */
759
- description: z.string().optional(),
760
- /** Short one-line pitch shown under the name (Ryu extension). */
761
- tagline: z.string().optional(),
762
- /**
763
- * Publisher identity. A bare string OR a Claude-style object; `ryu publish`
764
- * resolves it to the display `developer` (`author.name` when an object).
765
- */
766
- author: z
767
- .union([
768
- z.string(),
769
- z.object({
770
- name: z.string(),
771
- email: z.string().optional(),
772
- url: z.string().optional(),
773
- }),
774
- ])
775
- .optional(),
776
- /** Project/marketing homepage — maps to the listing `website` (Claude field). */
777
- homepage: z.string().optional(),
778
- /** Free-text search keywords (Claude field). */
779
- keywords: z.array(z.string()).optional(),
780
- /** Taxonomy category beyond the runnable kinds (Claude field). */
781
- category: z.string().optional(),
782
- /** SPDX-ish license identifier (Claude field). */
783
- license: z.string().optional(),
784
- /** Square logo/icon URL for the listing card + detail header. */
785
- iconUrl: z.string().optional(),
786
- /**
787
- * Icon-primitive id for the listing card (Ryu extension): an Iconify/icons0
788
- * `prefix:name`, a bare Hugeicons name, or a URL, resolved by the shared `Icon`
789
- * primitive. A monochrome GLYPH masked with the current text colour — distinct
790
- * from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
791
- */
792
- icon: z.string().optional(),
793
- /**
794
- * Dithered-gradient background for the card's icon square (Ryu extension),
795
- * mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
796
- * name (`green`, `blue`, `purple`, `pink`, `orange`, `red`, `grey`) or a hue
797
- * number (0–360); `direction` is where `to` ends up. Renders behind the glyph in
798
- * place of a flat `iconBackground`; the render layer validates + falls back.
799
- */
800
- iconDither: z
801
- .object({
802
- from: z.union([z.string(), z.number()]),
803
- to: z.union([z.string(), z.number()]).optional(),
804
- direction: z.enum(["up", "down", "left", "right"]).optional(),
805
- })
806
- .optional(),
807
- /** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
808
- screenshots: z.array(z.string()).optional(),
809
- /** Privacy policy URL surfaced on detail (Ryu extension). */
810
- privacyPolicyUrl: z.string().optional(),
811
- /** Terms-of-service URL surfaced on detail (Ryu extension). */
812
- termsOfServiceUrl: z.string().optional(),
813
- /**
814
- * Human-readable capability strings (Ryu extension). When omitted the control
815
- * plane derives a default from `permission_grants`, so declaring this is only
816
- * needed to override the derived labels.
817
- */
818
- capabilities: z.array(z.string()).optional(),
819
- /** Example prompt chips shown on detail (Ryu extension). */
820
- examplePrompts: z.array(z.string()).optional(),
821
- /**
822
- * Optional companion/config card (Ryu extension): a single setup step or an
823
- * array of steps guiding the user through post-install configuration.
824
- */
825
- setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional(),
826
- });
831
+ /**
832
+ * Host version floors the semver requirement each surface must satisfy for
833
+ * this plugin to install. Mirrors Core's `EnginesReq`
834
+ * (`crates/core/kernel-contracts/src/manifest.rs`).
835
+ *
836
+ * `ryu` is the **Core** floor and the only required key (it is the legacy
837
+ * spelling; every manifest in the wild carries just that one). The rest are
838
+ * optional per-surface floors.
839
+ *
840
+ * REGRESSION THIS FIXES: `engines` was absent from this schema entirely, and
841
+ * zod strips unlisted keys — so `ryu pack` silently dropped the whole block
842
+ * from every bundle it produced. A plugin could declare a Core floor, publish,
843
+ * and ship a bundle that declared none. Any new host floor MUST be added here
844
+ * as well as in the Rust contract, or it does not survive packing.
845
+ */
846
+ engines: EnginesReqSchema.optional(),
847
+
848
+ /**
849
+ * Optional per-item AFFILIATE terms: the commission paid to a referrer when a
850
+ * referred user buys this (paid) item. `value` is basis points for `percent`
851
+ * (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
852
+ * falls back to the seller org owner's default affiliate terms. This is the
853
+ * authoring surface for the marketplace publish body's `affiliate` field (the
854
+ * server re-validates it); it only takes effect on a paid item.
855
+ */
856
+ affiliate: z
857
+ .object({
858
+ enabled: z.boolean().default(false),
859
+ rule: z
860
+ .object({
861
+ type: z.enum(["percent", "flat"]),
862
+ value: z.number().nonnegative(),
863
+ recurring: z.boolean().default(false),
864
+ durationMonths: z.number().int().positive().nullish(),
865
+ fundedBy: z.enum(["platform", "seller"]).default("platform"),
866
+ })
867
+ .optional(),
868
+ })
869
+ .optional(),
870
+
871
+ // ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
872
+ // Optional store-listing fields a plugin author declares so the marketplace
873
+ // detail dialog renders a richer App-Store-style preview. Field names align
874
+ // with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
875
+ // one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
876
+ // rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
877
+ // body (not inside the signed manifest blob) so the control plane stores them.
878
+ // All optional + additive: a manifest omitting them still validates.
879
+
880
+ /** Longer plain/markdown description shown in the detail dialog. */
881
+ description: z.string().optional(),
882
+ /** Short one-line pitch shown under the name (Ryu extension). */
883
+ tagline: z.string().optional(),
884
+ /**
885
+ * Publisher identity. A bare string OR a Claude-style object; `ryu publish`
886
+ * resolves it to the display `developer` (`author.name` when an object).
887
+ */
888
+ author: z
889
+ .union([
890
+ z.string(),
891
+ z.object({
892
+ name: z.string(),
893
+ email: z.string().optional(),
894
+ url: z.string().optional(),
895
+ }),
896
+ ])
897
+ .optional(),
898
+ /** Public source repository URL (Claude/Codex `repository`). */
899
+ repository: z.string().url().optional(),
900
+ /** True when the provider operates outside the local Ryu runtime. */
901
+ external: z.boolean().optional(),
902
+ /** Project/marketing homepage maps to the listing `website` (Claude field). */
903
+ homepage: z.string().optional(),
904
+ /** Free-text search keywords (Claude field). */
905
+ keywords: z.array(z.string()).optional(),
906
+ /** Stable Marketplace filter labels (Ryu extension). */
907
+ tags: z.array(z.string()).optional(),
908
+ /** Taxonomy category beyond the runnable kinds (Claude field). */
909
+ category: z.string().optional(),
910
+ /** SPDX-ish license identifier (Claude field). */
911
+ license: z.string().optional(),
912
+ /** Square logo/icon URL for the listing card + detail header. */
913
+ iconUrl: z.string().optional(),
914
+ /**
915
+ * Icon-primitive id for the listing card (Ryu extension): an Iconify/icons0
916
+ * `prefix:name`, a bare Hugeicons name, or a URL, resolved by the shared `Icon`
917
+ * primitive. A monochrome GLYPH masked with the current text colour — distinct
918
+ * from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
919
+ */
920
+ icon: z.string().optional(),
921
+ /** Optional detail-page hero banner metadata forwarded to the marketplace. */
922
+ banner: z.record(z.string(), z.unknown()).optional(),
923
+ /**
924
+ * Dithered-gradient background for the card's icon square (Ryu extension),
925
+ * mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
926
+ * name (`green`, `blue`, `purple`, `pink`, `orange`, `red`, `grey`) or a hue
927
+ * number (0–360); `direction` is where `to` ends up. Renders behind the glyph in
928
+ * place of a flat `iconBackground`; the render layer validates + falls back.
929
+ */
930
+ iconDither: z
931
+ .object({
932
+ from: z.union([z.string(), z.number()]),
933
+ to: z.union([z.string(), z.number()]).optional(),
934
+ direction: z.enum(["up", "down", "left", "right"]).optional(),
935
+ })
936
+ .optional(),
937
+ /** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
938
+ screenshots: z.array(z.string()).optional(),
939
+ /** Privacy policy URL surfaced on detail (Ryu extension). */
940
+ privacyPolicyUrl: z.string().optional(),
941
+ /** Terms-of-service URL surfaced on detail (Ryu extension). */
942
+ termsOfServiceUrl: z.string().optional(),
943
+ /**
944
+ * Human-readable capability strings (Ryu extension). When omitted the control
945
+ * plane derives a default from `permission_grants`, so declaring this is only
946
+ * needed to override the derived labels.
947
+ */
948
+ capabilities: z.array(z.string()).optional(),
949
+ /** Example prompt chips shown on detail (Ryu extension). */
950
+ examplePrompts: z.array(z.string()).optional(),
951
+ /**
952
+ * Optional companion/config card (Ryu extension): a single setup step or an
953
+ * array of steps guiding the user through post-install configuration.
954
+ */
955
+ setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional(),
956
+ })
957
+ .superRefine((manifest, context) => {
958
+ const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
959
+ (server) => server.auth?.type === "oauth"
960
+ );
961
+ if (!hasOAuthServer) {
962
+ return;
963
+ }
964
+ for (const grant of ["mcp:server", "identity.read"] as const) {
965
+ if (!manifest.permission_grants.includes(grant)) {
966
+ context.addIssue({
967
+ code: "custom",
968
+ message: `OAuth MCP servers require the ${grant} permission grant`,
969
+ path: ["permission_grants"],
970
+ });
971
+ }
972
+ }
973
+ });
827
974
 
828
975
  export type PluginManifest = z.infer<typeof PluginManifestSchema>;
829
976
 
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @ryuhq/sdk/mcp — MCP server + client surfaces.
3
+ *
4
+ * `McpServer` handles the MCP `initialize` handshake, `tools/list`,
5
+ * `tools/call`, and the content-block envelope over stdio; `listTools` /
6
+ * `callTool` drive a stdio MCP server. Re-exported here so consumers can
7
+ * import from `@ryuhq/sdk/mcp` (or the narrower `/mcp/server` and
8
+ * `/mcp/client` subpaths) as a single entry point.
9
+ */
10
+
11
+ export type { McpStdioCommand, McpTool } from "./client.ts";
12
+ export { callTool, listTools, MCP_PROTOCOL_VERSION } from "./client.ts";
13
+ export type {
14
+ JsonSchema,
15
+ PassthroughRegistration,
16
+ SdkRunnable,
17
+ } from "./server.ts";
18
+ export { McpServer, unwrapContent } from "./server.ts";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @ryuhq/sdk/model — the gateway-mandatory model client.
3
+ *
4
+ * Every model call routes through the Ryu Gateway, never a direct provider.
5
+ * Re-exports the `ModelClient` / `defineModel` surface plus the gateway
6
+ * resolution + egress helpers, so consumers can import from
7
+ * `@ryuhq/sdk/model` as a single entry point.
8
+ */
9
+
10
+ export type {
11
+ ChatDelta,
12
+ ChatMessage,
13
+ ChatResult,
14
+ ModelClientOptions,
15
+ } from "./client.ts";
16
+ export { defineModel, ModelClient } from "./client.ts";
17
+ export {
18
+ assertAllowedEgressUrl,
19
+ DEFAULT_GATEWAY_URL,
20
+ resolveGatewayToken,
21
+ resolveGatewayUrl,
22
+ } from "./gateway.ts";