@ryuhq/sdk 0.1.13 → 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/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,30 @@ 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({ code: "custom", path: ["backing"], message: "available templates need exactly one backing tool_id or view_id" });
696
+ }
697
+ if (count > 1) {
698
+ ctx.addIssue({ code: "custom", path: ["backing"], message: "backing must declare at most one of tool_id or view_id" });
699
+ }
700
+ });
633
701
  var ToolAppConfigSchema = import_zod.z.object({
634
- /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
702
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
635
703
  slug: import_zod.z.string().min(1),
636
704
  /** The tool description the model reads when choosing it. Carried here because a
637
705
  * packed app's manifest is the only channel (there is no `generated.rs`); Core's
@@ -658,6 +726,11 @@ var ContributesSchema = import_zod.z.object({
658
726
  * signing, leaving an app that emits events nothing is allowed to subscribe to. */
659
727
  hook_events: import_zod.z.array(HookEventContributionSchema).default([]),
660
728
  composer_controls: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
729
+ /** Chat feature descriptors whose behavior is implemented by the host shell.
730
+ * Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
731
+ * authoring schema prevents `ryu pack` from silently deleting a plugin's chat
732
+ * feature declaration before signing. */
733
+ chat_features: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
661
734
  settings_tabs: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
662
735
  slash_commands: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
663
736
  /** App widgets (Ryu Apps). Each binds a render tool id to its
@@ -665,6 +738,8 @@ var ContributesSchema = import_zod.z.object({
665
738
  * `Contributes.widgets` field, without which the CLI's zod parse would strip
666
739
  * every widget an app authored here declares. */
667
740
  widgets: import_zod.z.array(WidgetContributionSchema).default([]),
741
+ /** Metadata-only chat widget templates. */
742
+ chat_widget_templates: import_zod.z.array(ChatWidgetTemplateSchema).optional(),
668
743
  /** App-registered sidebar sections (header + live list) and buttons (single nav
669
744
  * rows). Loosely typed here — the shell owns the spec vocabulary — matching how
670
745
  * `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
@@ -677,6 +752,13 @@ var ContributesSchema = import_zod.z.object({
677
752
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
678
753
  * strip the dock panel an app declares here. */
679
754
  dock_panels: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
755
+ /** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
756
+ * typed for the same reason as the surfaces above — the shell owns the
757
+ * `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
758
+ * without it the CLI's zod parse would strip every live activity an app
759
+ * declares here, so a packed bundle would ship a dock that silently stays
760
+ * empty. */
761
+ live_activities: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
680
762
  /** Deletable data categories the app owns — one "Delete all X" row in Settings
681
763
  * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
682
764
  * it the CLI's zod parse would strip the declaration before signing, and the
@@ -714,7 +796,10 @@ var ContributesSchema = import_zod.z.object({
714
796
  * bug: the styles' `.md` files are not carried by the bundle either, so there
715
797
  * would be no residue to notice — the plugin would install clean and contribute
716
798
  * nothing. */
717
- output_styles: import_zod.z.array(OutputStyleContributionSchema).default([])
799
+ output_styles: import_zod.z.array(OutputStyleContributionSchema).default([]),
800
+ /** Per-message actions contributed by an enabled plugin. Kept as loose records
801
+ * so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
802
+ message_actions: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([])
718
803
  });
719
804
  var SetupStepSchema = import_zod.z.object({
720
805
  /** Card heading (e.g. the companion app name). */
@@ -795,6 +880,61 @@ var EnginesReqSchema = import_zod.z.object({
795
880
  /** Floor for the Next.js web app. */
796
881
  web: import_zod.z.string().optional()
797
882
  });
883
+ var McpServerAuthSchema = import_zod.z.object({
884
+ client_id: import_zod.z.string().min(1).optional(),
885
+ type: import_zod.z.literal("oauth")
886
+ }).strict();
887
+ var McpServerDeclSchema = import_zod.z.object({
888
+ args: import_zod.z.array(import_zod.z.string()).default([]),
889
+ auth: McpServerAuthSchema.optional(),
890
+ command: import_zod.z.string().optional(),
891
+ command_env: import_zod.z.string().optional(),
892
+ description: import_zod.z.string().optional(),
893
+ enabled: import_zod.z.boolean().default(true),
894
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}),
895
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}),
896
+ type: import_zod.z.enum(["stdio", "http", "streamable-http", "streamable_http", "sse"]).optional(),
897
+ url: import_zod.z.url().optional()
898
+ }).superRefine((server, context) => {
899
+ if (!(server.command || server.url)) {
900
+ context.addIssue({
901
+ code: "custom",
902
+ message: "an MCP server requires command or url"
903
+ });
904
+ }
905
+ if (!server.auth) {
906
+ return;
907
+ }
908
+ if (server.command || server.type === "stdio" || !server.url) {
909
+ context.addIssue({
910
+ code: "custom",
911
+ message: "OAuth is supported only for remote HTTP MCP servers"
912
+ });
913
+ return;
914
+ }
915
+ if (Object.keys(server.headers).some(
916
+ (name) => name.toLowerCase() === "authorization"
917
+ )) {
918
+ context.addIssue({
919
+ code: "custom",
920
+ message: "OAuth cannot be combined with a static Authorization header"
921
+ });
922
+ }
923
+ const url = new URL(server.url);
924
+ const loopback = url.hostname === "localhost" || url.hostname.startsWith("127.") || url.hostname === "[::1]" || url.hostname === "::1";
925
+ if (url.username || url.password || url.hash) {
926
+ context.addIssue({
927
+ code: "custom",
928
+ message: "OAuth MCP URLs cannot contain credentials or fragments"
929
+ });
930
+ }
931
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
932
+ context.addIssue({
933
+ code: "custom",
934
+ message: "OAuth MCP URLs must use HTTPS except on loopback"
935
+ });
936
+ }
937
+ });
798
938
  var PluginManifestSchema = import_zod.z.object({
799
939
  /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
800
940
  id: import_zod.z.string().min(1, "id is required"),
@@ -826,6 +966,8 @@ var PluginManifestSchema = import_zod.z.object({
826
966
  * Declarations only — grant enforcement is the Gateway's responsibility.
827
967
  */
828
968
  permission_grants: import_zod.z.array(import_zod.z.string()).default([]),
969
+ /** Remote or stdio MCP servers registered by this plugin. */
970
+ mcp_servers: import_zod.z.record(import_zod.z.string(), McpServerDeclSchema).optional(),
829
971
  /**
830
972
  * Optional Companion surface (an in-desktop overlay or sidebar panel).
831
973
  * Absent when the plugin has no Companion surface.
@@ -919,10 +1061,16 @@ var PluginManifestSchema = import_zod.z.object({
919
1061
  url: import_zod.z.string().optional()
920
1062
  })
921
1063
  ]).optional(),
1064
+ /** Public source repository URL (Claude/Codex `repository`). */
1065
+ repository: import_zod.z.string().url().optional(),
1066
+ /** True when the provider operates outside the local Ryu runtime. */
1067
+ external: import_zod.z.boolean().optional(),
922
1068
  /** Project/marketing homepage — maps to the listing `website` (Claude field). */
923
1069
  homepage: import_zod.z.string().optional(),
924
1070
  /** Free-text search keywords (Claude field). */
925
1071
  keywords: import_zod.z.array(import_zod.z.string()).optional(),
1072
+ /** Stable Marketplace filter labels (Ryu extension). */
1073
+ tags: import_zod.z.array(import_zod.z.string()).optional(),
926
1074
  /** Taxonomy category beyond the runnable kinds (Claude field). */
927
1075
  category: import_zod.z.string().optional(),
928
1076
  /** SPDX-ish license identifier (Claude field). */
@@ -936,6 +1084,8 @@ var PluginManifestSchema = import_zod.z.object({
936
1084
  * from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
937
1085
  */
938
1086
  icon: import_zod.z.string().optional(),
1087
+ /** Optional detail-page hero banner metadata forwarded to the marketplace. */
1088
+ banner: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
939
1089
  /**
940
1090
  * Dithered-gradient background for the card's icon square (Ryu extension),
941
1091
  * mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
@@ -967,6 +1117,22 @@ var PluginManifestSchema = import_zod.z.object({
967
1117
  * array of steps guiding the user through post-install configuration.
968
1118
  */
969
1119
  setup: import_zod.z.union([SetupStepSchema, import_zod.z.array(SetupStepSchema)]).optional()
1120
+ }).superRefine((manifest, context) => {
1121
+ const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
1122
+ (server) => server.auth?.type === "oauth"
1123
+ );
1124
+ if (!hasOAuthServer) {
1125
+ return;
1126
+ }
1127
+ for (const grant of ["mcp:server", "identity.read"]) {
1128
+ if (!manifest.permission_grants.includes(grant)) {
1129
+ context.addIssue({
1130
+ code: "custom",
1131
+ message: `OAuth MCP servers require the ${grant} permission grant`,
1132
+ path: ["permission_grants"]
1133
+ });
1134
+ }
1135
+ }
970
1136
  });
971
1137
 
972
1138
  // src/cli.ts
@@ -980,7 +1146,9 @@ function printUsage() {
980
1146
  "",
981
1147
  "Usage:",
982
1148
  " 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",
1149
+ " bunx ryu github-publish <url>",
1150
+ " Submit a GitHub repository/package URL to the Marketplace",
1151
+ " bunx ryu publish <dir> Deprecated server-artifact publishing (migration only)",
984
1152
  " bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
985
1153
  " bunx ryu agent-plugin <dir>",
986
1154
  " Emit the Agent Plugins v1 interop pair (plugin.json + mcp.json)",
@@ -999,16 +1167,20 @@ var MANIFEST_FILE_NAMES = [
999
1167
  "ryu.json"
1000
1168
  ];
1001
1169
  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;
1170
+ return MANIFEST_FILE_NAMES.map((name) => (0, import_node_path.join)(dir, name)).find(
1171
+ (candidate) => {
1172
+ if (!(0, import_node_fs.existsSync)(candidate)) {
1173
+ return false;
1174
+ }
1175
+ try {
1176
+ return !isAgentPluginManifest(
1177
+ JSON.parse((0, import_node_fs.readFileSync)(candidate, "utf8"))
1178
+ );
1179
+ } catch {
1180
+ return true;
1181
+ }
1010
1182
  }
1011
- });
1183
+ );
1012
1184
  }
1013
1185
  function loadManifest(dir) {
1014
1186
  const manifestPath = resolveNativeManifestPath(dir);
@@ -1271,8 +1443,16 @@ async function commandPublish(rawDir) {
1271
1443
  ...manifest.description ? { description: manifest.description } : {},
1272
1444
  ...manifest.tagline ? { tagline: manifest.tagline } : {},
1273
1445
  ...developer ? { developer } : {},
1446
+ ...manifest.author ? { author: manifest.author } : {},
1447
+ ...manifest.repository ? { repository: manifest.repository } : {},
1448
+ ...manifest.external ? { external: true } : {},
1274
1449
  ...manifest.category ? { category: manifest.category } : {},
1450
+ ...manifest.license ? { license: manifest.license } : {},
1451
+ ...manifest.keywords?.length ? { keywords: manifest.keywords } : {},
1275
1452
  ...manifest.iconUrl ? { iconUrl: manifest.iconUrl } : {},
1453
+ ...manifest.icon ? { icon: manifest.icon } : {},
1454
+ ...manifest.iconDither ? { iconDither: manifest.iconDither } : {},
1455
+ ...manifest.banner ? { banner: manifest.banner } : {},
1276
1456
  ...manifest.screenshots?.length ? { screenshots: manifest.screenshots } : {},
1277
1457
  ...manifest.homepage ? { website: manifest.homepage } : {},
1278
1458
  ...manifest.privacyPolicyUrl ? { privacyPolicyUrl: manifest.privacyPolicyUrl } : {},
@@ -1328,6 +1508,55 @@ ${text}
1328
1508
  `
1329
1509
  );
1330
1510
  }
1511
+ async function commandGithubPublish(repositoryUrl) {
1512
+ const input = repositoryUrl.trim();
1513
+ if (!input) {
1514
+ exitError(
1515
+ "github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
1516
+ );
1517
+ }
1518
+ const token = authToken();
1519
+ const url = `${publishBaseUrl()}/api/marketplace/github/publish`;
1520
+ const proof = process.env.RYU_GITHUB_INSTALLATION_PROOF?.trim();
1521
+ const body = {
1522
+ url: input,
1523
+ ...proof ? { installationProof: proof } : {}
1524
+ };
1525
+ let response;
1526
+ try {
1527
+ response = await fetch(url, {
1528
+ method: "POST",
1529
+ headers: {
1530
+ authorization: `Bearer ${token}`,
1531
+ "content-type": "application/json"
1532
+ },
1533
+ body: JSON.stringify(body)
1534
+ });
1535
+ } catch (error) {
1536
+ exitError(`could not reach ${url}: ${String(error)}`);
1537
+ }
1538
+ const text = await response.text();
1539
+ if (!response.ok) {
1540
+ try {
1541
+ const error = JSON.parse(text);
1542
+ if (error.installationUrl) {
1543
+ exitError(
1544
+ `${error.error ?? "GitHub App installation required"}. Open ${error.installationUrl}, then set RYU_GITHUB_INSTALLATION_PROOF to the callback proof and retry.`
1545
+ );
1546
+ }
1547
+ exitError(
1548
+ `github-publish failed (${response.status}): ${error.error ?? text}`
1549
+ );
1550
+ } catch {
1551
+ exitError(`github-publish failed (${response.status}): ${text}`);
1552
+ }
1553
+ }
1554
+ process.stdout.write(
1555
+ `GitHub-backed listing submitted for moderation:
1556
+ ${text}
1557
+ `
1558
+ );
1559
+ }
1331
1560
  var [, , command, ...args] = process.argv;
1332
1561
  if (!command) {
1333
1562
  printUsage();
@@ -1346,9 +1575,24 @@ if (command === "pack") {
1346
1575
  if (!dir) {
1347
1576
  exitError("publish requires a directory argument: bunx ryu publish <dir>");
1348
1577
  }
1578
+ if (process.env.RYU_ENABLE_LEGACY_MARKETPLACE_WRITES !== "true") {
1579
+ exitError(
1580
+ "ryu publish is deprecated because marketplace package contents now live in GitHub Releases; use bunx ryu github-publish <repository-or-package-url>"
1581
+ );
1582
+ }
1349
1583
  commandPublish(dir).catch((err) => {
1350
1584
  exitError(String(err));
1351
1585
  });
1586
+ } else if (command === "github-publish") {
1587
+ const repositoryUrl = args[0];
1588
+ if (!repositoryUrl) {
1589
+ exitError(
1590
+ "github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
1591
+ );
1592
+ }
1593
+ commandGithubPublish(repositoryUrl).catch((err) => {
1594
+ exitError(String(err));
1595
+ });
1352
1596
  } else if (command === "agent-plugin") {
1353
1597
  const dir = args[0];
1354
1598
  if (!dir) {
package/dist/cli.js CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env bun
2
2
  import {
3
3
  PluginManifestSchema
4
- } from "./chunk-AO2KJRDD.js";
4
+ } from "./chunk-IEUQ3CDG.js";
5
5
  import {
6
6
  AGENT_PLUGIN_MANIFEST_FILE,
7
7
  AGENT_PLUGIN_MCP_FILE,
8
8
  isAgentPluginManifest,
9
9
  toAgentPlugin
10
- } from "./chunk-G6FLVEC4.js";
10
+ } from "./chunk-JX6DDRXV.js";
11
+ import {
12
+ ModelClient
13
+ } from "./chunk-HACAGK65.js";
11
14
  import {
12
- ModelClient,
13
15
  resolveGatewayUrl
14
- } from "./chunk-ODFEUVPW.js";
16
+ } from "./chunk-IKEDLLFY.js";
15
17
 
16
18
  // src/cli.ts
17
19
  import { createHash } from "crypto";
@@ -27,9 +29,9 @@ import { join, resolve } from "path";
27
29
  // src/cli/dev.ts
28
30
  import { createInterface } from "readline";
29
31
  var RE_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/;
30
- async function probeGateway(baseUrl) {
32
+ async function probeGateway(baseUrl, fetchImpl = fetch) {
31
33
  try {
32
- await fetch(`${baseUrl}/health`, {
34
+ await fetchImpl(`${baseUrl}/health`, {
33
35
  method: "HEAD",
34
36
  signal: AbortSignal.timeout(3e3)
35
37
  });
@@ -228,7 +230,9 @@ function printUsage() {
228
230
  "",
229
231
  "Usage:",
230
232
  " bunx ryu pack <dir> Validate and bundle a manifest.json Plugin",
231
- " bunx ryu publish <dir> Validate and publish a manifest.json Plugin to the Ryu Marketplace",
233
+ " bunx ryu github-publish <url>",
234
+ " Submit a GitHub repository/package URL to the Marketplace",
235
+ " bunx ryu publish <dir> Deprecated server-artifact publishing (migration only)",
232
236
  " bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
233
237
  " bunx ryu agent-plugin <dir>",
234
238
  " Emit the Agent Plugins v1 interop pair (plugin.json + mcp.json)",
@@ -247,16 +251,20 @@ var MANIFEST_FILE_NAMES = [
247
251
  "ryu.json"
248
252
  ];
249
253
  function resolveNativeManifestPath(dir) {
250
- return MANIFEST_FILE_NAMES.map((name) => join(dir, name)).find((candidate) => {
251
- if (!existsSync(candidate)) {
252
- return false;
253
- }
254
- try {
255
- return !isAgentPluginManifest(JSON.parse(readFileSync(candidate, "utf8")));
256
- } catch {
257
- return true;
254
+ return MANIFEST_FILE_NAMES.map((name) => join(dir, name)).find(
255
+ (candidate) => {
256
+ if (!existsSync(candidate)) {
257
+ return false;
258
+ }
259
+ try {
260
+ return !isAgentPluginManifest(
261
+ JSON.parse(readFileSync(candidate, "utf8"))
262
+ );
263
+ } catch {
264
+ return true;
265
+ }
258
266
  }
259
- });
267
+ );
260
268
  }
261
269
  function loadManifest(dir) {
262
270
  const manifestPath = resolveNativeManifestPath(dir);
@@ -519,8 +527,16 @@ async function commandPublish(rawDir) {
519
527
  ...manifest.description ? { description: manifest.description } : {},
520
528
  ...manifest.tagline ? { tagline: manifest.tagline } : {},
521
529
  ...developer ? { developer } : {},
530
+ ...manifest.author ? { author: manifest.author } : {},
531
+ ...manifest.repository ? { repository: manifest.repository } : {},
532
+ ...manifest.external ? { external: true } : {},
522
533
  ...manifest.category ? { category: manifest.category } : {},
534
+ ...manifest.license ? { license: manifest.license } : {},
535
+ ...manifest.keywords?.length ? { keywords: manifest.keywords } : {},
523
536
  ...manifest.iconUrl ? { iconUrl: manifest.iconUrl } : {},
537
+ ...manifest.icon ? { icon: manifest.icon } : {},
538
+ ...manifest.iconDither ? { iconDither: manifest.iconDither } : {},
539
+ ...manifest.banner ? { banner: manifest.banner } : {},
524
540
  ...manifest.screenshots?.length ? { screenshots: manifest.screenshots } : {},
525
541
  ...manifest.homepage ? { website: manifest.homepage } : {},
526
542
  ...manifest.privacyPolicyUrl ? { privacyPolicyUrl: manifest.privacyPolicyUrl } : {},
@@ -576,6 +592,55 @@ ${text}
576
592
  `
577
593
  );
578
594
  }
595
+ async function commandGithubPublish(repositoryUrl) {
596
+ const input = repositoryUrl.trim();
597
+ if (!input) {
598
+ exitError(
599
+ "github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
600
+ );
601
+ }
602
+ const token = authToken();
603
+ const url = `${publishBaseUrl()}/api/marketplace/github/publish`;
604
+ const proof = process.env.RYU_GITHUB_INSTALLATION_PROOF?.trim();
605
+ const body = {
606
+ url: input,
607
+ ...proof ? { installationProof: proof } : {}
608
+ };
609
+ let response;
610
+ try {
611
+ response = await fetch(url, {
612
+ method: "POST",
613
+ headers: {
614
+ authorization: `Bearer ${token}`,
615
+ "content-type": "application/json"
616
+ },
617
+ body: JSON.stringify(body)
618
+ });
619
+ } catch (error) {
620
+ exitError(`could not reach ${url}: ${String(error)}`);
621
+ }
622
+ const text = await response.text();
623
+ if (!response.ok) {
624
+ try {
625
+ const error = JSON.parse(text);
626
+ if (error.installationUrl) {
627
+ exitError(
628
+ `${error.error ?? "GitHub App installation required"}. Open ${error.installationUrl}, then set RYU_GITHUB_INSTALLATION_PROOF to the callback proof and retry.`
629
+ );
630
+ }
631
+ exitError(
632
+ `github-publish failed (${response.status}): ${error.error ?? text}`
633
+ );
634
+ } catch {
635
+ exitError(`github-publish failed (${response.status}): ${text}`);
636
+ }
637
+ }
638
+ process.stdout.write(
639
+ `GitHub-backed listing submitted for moderation:
640
+ ${text}
641
+ `
642
+ );
643
+ }
579
644
  var [, , command, ...args] = process.argv;
580
645
  if (!command) {
581
646
  printUsage();
@@ -594,9 +659,24 @@ if (command === "pack") {
594
659
  if (!dir) {
595
660
  exitError("publish requires a directory argument: bunx ryu publish <dir>");
596
661
  }
662
+ if (process.env.RYU_ENABLE_LEGACY_MARKETPLACE_WRITES !== "true") {
663
+ exitError(
664
+ "ryu publish is deprecated because marketplace package contents now live in GitHub Releases; use bunx ryu github-publish <repository-or-package-url>"
665
+ );
666
+ }
597
667
  commandPublish(dir).catch((err) => {
598
668
  exitError(String(err));
599
669
  });
670
+ } else if (command === "github-publish") {
671
+ const repositoryUrl = args[0];
672
+ if (!repositoryUrl) {
673
+ exitError(
674
+ "github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
675
+ );
676
+ }
677
+ commandGithubPublish(repositoryUrl).catch((err) => {
678
+ exitError(String(err));
679
+ });
600
680
  } else if (command === "agent-plugin") {
601
681
  const dir = args[0];
602
682
  if (!dir) {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Gateway-mandatory model client for the Ryu SDK.
3
+ *
4
+ * Rust-cored: this is a thin TypeScript wrapper over the `@ryuhq/sdk-native`
5
+ * addon's `ModelClient`, which is the `crates/ryu-sdk` Rust core. Every model
6
+ * call is routed by the Rust core to the Ryu gateway's OpenAI-compatible
7
+ * `/v1/chat/completions` endpoint; direct-provider base URLs are rejected at
8
+ * construction. No provider SDK or base URL is ever imported here.
9
+ *
10
+ * Usage:
11
+ *
12
+ * const model = defineModel("gpt-4o");
13
+ * const reply = await model.chat([{ role: "user", content: "hello" }]);
14
+ * for await (const delta of model.stream([{ role: "user", content: "hi" }])) {
15
+ * process.stdout.write(delta.content);
16
+ * }
17
+ */
18
+ /** A single message in a chat conversation. */
19
+ interface ChatMessage {
20
+ /** The message text. */
21
+ content: string;
22
+ /** The speaker: "system", "user", or "assistant". */
23
+ role: "system" | "user" | "assistant";
24
+ }
25
+ /** A streaming chat completion delta. */
26
+ interface ChatDelta {
27
+ /** Incremental text fragment from the model. */
28
+ content: string | null;
29
+ /** Non-null on the final chunk when `finish_reason` is set. */
30
+ finishReason: string | null;
31
+ }
32
+ /** Non-streaming chat completion result. */
33
+ interface ChatResult {
34
+ /** The full assistant reply text. */
35
+ content: string;
36
+ /** The gateway/model-reported finish reason. */
37
+ finishReason: string | null;
38
+ /** Usage stats as reported by the gateway (optional — gateway may omit). */
39
+ usage?: {
40
+ promptTokens: number;
41
+ completionTokens: number;
42
+ totalTokens: number;
43
+ };
44
+ }
45
+ /** Options accepted by `defineModel`. */
46
+ interface ModelClientOptions {
47
+ /**
48
+ * Gateway base URL (no trailing `/v1`). Defaults to `RYU_GATEWAY_URL` then
49
+ * the Rust core's default. Direct provider URLs are rejected at construction.
50
+ */
51
+ baseUrl?: string;
52
+ /**
53
+ * Bearer token forwarded to the gateway. Defaults to `RYU_GATEWAY_TOKEN`.
54
+ * This is the gateway token, never a provider API key.
55
+ */
56
+ token?: string;
57
+ }
58
+ /**
59
+ * A gateway-mandatory model client backed by the Rust core.
60
+ *
61
+ * All calls go through the native `ModelClient`; if the configured base URL is a
62
+ * direct provider, construction throws.
63
+ */
64
+ declare class ModelClient {
65
+ readonly model: string;
66
+ readonly baseUrl: string;
67
+ private readonly native;
68
+ constructor(model: string, options?: ModelClientOptions);
69
+ /** Send a non-streaming chat completion request to the gateway. */
70
+ chat(messages: ChatMessage[]): Promise<ChatResult>;
71
+ /** Send a streaming chat completion request, yielding deltas as they arrive. */
72
+ stream(messages: ChatMessage[]): AsyncGenerator<ChatDelta>;
73
+ }
74
+ /**
75
+ * Create a gateway-mandatory model client for `modelId`.
76
+ *
77
+ * const model = defineModel("gpt-4o");
78
+ * const model = defineModel("claude-3-5-sonnet", { baseUrl: "http://my-gateway:7981" });
79
+ *
80
+ * A direct provider URL throws immediately (egress enforcement in the Rust core).
81
+ */
82
+ declare function defineModel(modelId: string, options?: ModelClientOptions): ModelClient;
83
+
84
+ export { type ChatDelta as C, ModelClient as M, type ChatMessage as a, type ChatResult as b, type ModelClientOptions as c, defineModel as d };