@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.
- package/README.md +69 -7
- package/dist/action.cjs +839 -0
- package/dist/action.d.cts +88 -0
- package/dist/action.d.ts +88 -0
- package/dist/action.js +8 -0
- package/dist/agent-plugin.cjs +46 -0
- package/dist/agent-plugin.d.cts +43 -33
- package/dist/agent-plugin.d.ts +43 -33
- package/dist/agent-plugin.js +1 -1
- package/dist/agent.cjs +16 -0
- package/dist/agent.d.cts +377 -1
- package/dist/agent.d.ts +377 -1
- package/dist/agent.js +4 -2
- package/dist/app-B0Z9Ew_R.d.cts +141 -0
- package/dist/app-C-BDJwfG.d.ts +141 -0
- package/dist/builder.cjs +1194 -0
- package/dist/builder.d.cts +217 -0
- package/dist/builder.d.ts +217 -0
- package/dist/builder.js +29 -0
- package/dist/chunk-4TPUZDTI.js +94 -0
- package/dist/chunk-BC3A7HMO.js +164 -0
- package/dist/chunk-FZSFZOIN.js +200 -0
- package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
- package/dist/chunk-HLKJZAFK.js +361 -0
- package/dist/chunk-IKEDLLFY.js +19 -0
- package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
- package/dist/{chunk-AO2KJRDD.js → chunk-NZKVOSC2.js} +192 -3
- package/dist/chunk-QYFUNJOH.js +83 -0
- package/dist/chunk-SN2QBJUF.js +148 -0
- package/dist/chunk-TLDPEGC7.js +21 -0
- package/dist/chunk-TXSHHZF2.js +0 -0
- package/dist/chunk-VLIRNNAE.js +154 -0
- package/dist/{chunk-MTUBUPIV.js → chunk-Z57QDDJR.js} +8 -227
- package/dist/chunk-ZTJWBRUL.js +282 -0
- package/dist/cli.cjs +347 -18
- package/dist/cli.js +128 -23
- package/dist/client-D5U6ssPc.d.cts +84 -0
- package/dist/client-D5U6ssPc.d.ts +84 -0
- package/dist/index.cjs +810 -86
- package/dist/index.d.cts +19 -634
- package/dist/index.d.ts +19 -634
- package/dist/index.js +82 -699
- package/dist/manifest.cjs +200 -4
- package/dist/manifest.d.cts +260 -5
- package/dist/manifest.d.ts +260 -5
- package/dist/manifest.js +15 -1
- package/dist/mcp/client.cjs +180 -0
- package/dist/mcp/client.d.cts +49 -0
- package/dist/mcp/client.d.ts +49 -0
- package/dist/mcp/client.js +10 -0
- package/dist/mcp/server.cjs +370 -0
- package/dist/mcp/server.d.cts +127 -0
- package/dist/mcp/server.d.ts +127 -0
- package/dist/mcp/server.js +9 -0
- package/dist/mcp.cjs +376 -0
- package/dist/mcp.d.cts +2 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +17 -0
- package/dist/model.cjs +141 -0
- package/dist/model.d.cts +33 -0
- package/dist/model.d.ts +33 -0
- package/dist/model.js +18 -0
- package/dist/plugin.cjs +46 -0
- package/dist/plugin.d.cts +215 -0
- package/dist/plugin.d.ts +215 -0
- package/dist/plugin.js +8 -0
- package/dist/runnable.cjs +1438 -0
- package/dist/runnable.d.cts +274 -0
- package/dist/runnable.d.ts +274 -0
- package/dist/runnable.js +34 -0
- package/dist/{index-B6SkaAjJ.d.ts → tool-AjkdFvhE.d.ts} +88 -461
- package/dist/{index-BvAB5eMk.d.cts → tool-CgzW92O_.d.cts} +88 -461
- package/package.json +48 -3
- package/src/agent/loop.test.ts +4 -4
- package/src/agent/tools.ts +3 -3
- package/src/agent-plugin.test.ts +58 -0
- package/src/agent-plugin.ts +109 -36
- package/src/builder.ts +9 -0
- package/src/cli/dev.test.ts +26 -47
- package/src/cli/dev.ts +10 -2
- package/src/cli-security.test.ts +109 -0
- package/src/cli.ts +141 -25
- package/src/contracts-lockstep.test.ts +16 -2
- package/src/exports-lockstep.test.ts +93 -0
- package/src/generated/plugin-manifest.ts +322 -29
- package/src/index.ts +53 -0
- package/src/manifest-schema.test.ts +30 -1
- package/src/manifest.fixtures.test.ts +22 -6
- package/src/manifest.test.ts +165 -10
- package/src/manifest.ts +454 -188
- package/src/mcp/index.ts +18 -0
- package/src/mcp/server.ts +2 -1
- package/src/model/index.ts +22 -0
- package/src/plugin/ryu-plugin.ts +82 -0
- package/src/runnable/action.test.ts +128 -0
- package/src/runnable/action.ts +202 -0
- package/src/runnable/app.test.ts +2 -0
- package/src/runnable/app.ts +50 -14
- package/src/runnable/index.ts +20 -3
- package/src/runnable/primitives.test.ts +34 -0
- package/src/runnable/primitives.ts +116 -0
- package/src/runnable/runnable-types.ts +3 -0
- package/src/runnable/tool.ts +36 -5
- package/src/runnable/turn-hook.ts +8 -3
- 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
|
|
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
|
|
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(
|
|
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 <
|
|
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.
|
|
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(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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 = (
|
|
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)((
|
|
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) {
|