@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
@@ -0,0 +1,250 @@
1
+ import {
2
+ assertAllowedEgressUrl
3
+ } from "./chunk-IKEDLLFY.js";
4
+
5
+ // src/runnable/primitives.ts
6
+ function dataUrlToBytes(dataUrl) {
7
+ const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
8
+ if (!match) {
9
+ throw new Error(
10
+ "stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
11
+ );
12
+ }
13
+ const mediaType = match[1] || "application/octet-stream";
14
+ const isBase64 = Boolean(match[2]);
15
+ const payload = match[3] ?? "";
16
+ if (isBase64) {
17
+ const binary = atob(payload);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let i = 0; i < binary.length; i++) {
20
+ bytes[i] = binary.charCodeAt(i);
21
+ }
22
+ return { bytes, mediaType };
23
+ }
24
+ return {
25
+ bytes: new TextEncoder().encode(decodeURIComponent(payload)),
26
+ mediaType
27
+ };
28
+ }
29
+ function bytesToDataUrl(bytes, mediaType) {
30
+ let binary = "";
31
+ const chunk = 32768;
32
+ for (let i = 0; i < bytes.length; i += chunk) {
33
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
34
+ }
35
+ return `data:${mediaType};base64,${btoa(binary)}`;
36
+ }
37
+ function httpPrimitiveTransport(options) {
38
+ const base = options.nodeUrl.replace(/\/+$/, "");
39
+ assertAllowedEgressUrl(base);
40
+ const doFetch = options.fetchImpl ?? fetch;
41
+ const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
42
+ const failDetail = async (path, res) => {
43
+ const detail = await res.text().catch(() => "");
44
+ return new Error(
45
+ `Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
46
+ );
47
+ };
48
+ const post = async (path, body) => {
49
+ const res = await doFetch(`${base}${path}`, {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json", ...authHeader() },
52
+ body: JSON.stringify(body ?? {})
53
+ });
54
+ if (!res.ok) {
55
+ throw await failDetail(path, res);
56
+ }
57
+ const text = await res.text();
58
+ return text ? JSON.parse(text) : void 0;
59
+ };
60
+ const transcribeDirect = async (body) => {
61
+ const input = body ?? {};
62
+ if (!input.audio) {
63
+ throw new Error(
64
+ "stt.transcribe requires an `audio` data: URL (the recorded audio)"
65
+ );
66
+ }
67
+ const { bytes, mediaType } = dataUrlToBytes(input.audio);
68
+ const form = new FormData();
69
+ form.append(
70
+ "file",
71
+ new Blob([bytes], { type: mediaType || "audio/wav" }),
72
+ input.filename ?? "recording.wav"
73
+ );
74
+ const res = await doFetch(`${base}/api/voice/transcribe`, {
75
+ method: "POST",
76
+ headers: authHeader(),
77
+ body: form
78
+ });
79
+ if (!res.ok) {
80
+ throw await failDetail("/api/voice/transcribe", res);
81
+ }
82
+ const parsed = await res.json();
83
+ return (parsed.text ?? "").trim();
84
+ };
85
+ const speakDirect = async (body) => {
86
+ const res = await doFetch(`${base}/api/voice/speak`, {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json", ...authHeader() },
89
+ body: JSON.stringify(body ?? {})
90
+ });
91
+ if (!res.ok) {
92
+ throw await failDetail("/api/voice/speak", res);
93
+ }
94
+ const bytes = new Uint8Array(await res.arrayBuffer());
95
+ const mediaType = res.headers.get("content-type") || "audio/wav";
96
+ return bytesToDataUrl(bytes, mediaType);
97
+ };
98
+ const generateImageDirect = async (body) => {
99
+ const res = await doFetch(`${base}/api/images/generate`, {
100
+ method: "POST",
101
+ headers: { "content-type": "application/json", ...authHeader() },
102
+ body: JSON.stringify(body ?? {})
103
+ });
104
+ if (!res.ok) {
105
+ throw await failDetail("/api/images/generate", res);
106
+ }
107
+ const parsed = await res.json();
108
+ return (parsed.data ?? []).map(
109
+ (item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
110
+ );
111
+ };
112
+ return {
113
+ bridge(method, args) {
114
+ if (!options.pluginId) {
115
+ return Promise.reject(
116
+ new Error(
117
+ `bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
118
+ )
119
+ );
120
+ }
121
+ return post(`/api/plugins/${options.pluginId}/host`, { method, args });
122
+ },
123
+ direct(path, body) {
124
+ if (path === "/api/voice/transcribe") {
125
+ return transcribeDirect(body);
126
+ }
127
+ if (path === "/api/voice/speak") {
128
+ return speakDirect(body);
129
+ }
130
+ if (path === "/api/images/generate") {
131
+ return generateImageDirect(body);
132
+ }
133
+ return post(path, body);
134
+ },
135
+ capability(cap, body) {
136
+ return post(`/api/host/capability/${cap}`, body);
137
+ }
138
+ };
139
+ }
140
+ var PRIMITIVE_BINDINGS = {
141
+ // Bridge families (existing `PluginHookBridge`).
142
+ "engines.complete": {
143
+ transport: "bridge",
144
+ method: "model.complete",
145
+ grant: "hook:side-model"
146
+ },
147
+ "background.list": {
148
+ transport: "bridge",
149
+ method: "background.list",
150
+ grant: "background:control"
151
+ },
152
+ "background.stop": {
153
+ transport: "bridge",
154
+ method: "background.stop",
155
+ grant: "background:control"
156
+ },
157
+ // Host-direct media data-path (the host holds the node token; returns data: URLs).
158
+ "image.generate": {
159
+ transport: "direct",
160
+ path: "/api/images/generate",
161
+ grant: "media:generate"
162
+ },
163
+ "tts.speak": {
164
+ transport: "direct",
165
+ path: "/api/voice/speak",
166
+ grant: "media:generate"
167
+ },
168
+ "stt.transcribe": {
169
+ transport: "direct",
170
+ path: "/api/voice/transcribe",
171
+ grant: "media:transcribe"
172
+ },
173
+ // Broker capabilities — no rpc family yet (@requires-grant).
174
+ "rag.retrieve": { transport: "broker", capability: "rag" },
175
+ "rag.embed": { transport: "broker", capability: "rag" },
176
+ "rag.rerank": { transport: "broker", capability: "rag" },
177
+ "memory.recall": { transport: "broker", capability: "memory" },
178
+ "memory.store": { transport: "broker", capability: "memory" },
179
+ "realtime.broadcast": { transport: "broker", capability: "realtime" },
180
+ "realtime.subscribe": { transport: "broker", capability: "realtime" },
181
+ "durable.checkpoint": { transport: "broker", capability: "durable" },
182
+ "durable.resume": { transport: "broker", capability: "durable" },
183
+ "engines.embed": { transport: "broker", capability: "engines" }
184
+ };
185
+ function brokerCall(transport, cap, op, input) {
186
+ return transport.capability(cap, { op, input });
187
+ }
188
+ function createPrimitives(transport) {
189
+ return {
190
+ background: {
191
+ list: (input) => transport.bridge("background.list", {
192
+ producer: input?.producer,
193
+ running_only: input?.runningOnly
194
+ }),
195
+ stop: (input) => transport.bridge("background.stop", {
196
+ process_id: input.processId
197
+ })
198
+ },
199
+ rag: {
200
+ retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
201
+ embed: (input) => brokerCall(transport, "rag", "embed", input),
202
+ rerank: (input) => brokerCall(transport, "rag", "rerank", input)
203
+ },
204
+ memory: {
205
+ recall: (input) => brokerCall(transport, "memory", "recall", input),
206
+ store: (input) => brokerCall(transport, "memory", "store", input)
207
+ },
208
+ realtime: {
209
+ broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
210
+ () => void 0
211
+ ),
212
+ subscribe: (input) => brokerCall(
213
+ transport,
214
+ "realtime",
215
+ "subscribe",
216
+ input
217
+ )
218
+ },
219
+ durable: {
220
+ checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
221
+ resume: (input) => brokerCall(transport, "durable", "resume", input)
222
+ },
223
+ engines: {
224
+ // Bridge family: model.complete → host.sideModel (wire keys are snake_case).
225
+ complete: (input) => transport.bridge("model.complete", {
226
+ prompt: input.prompt,
227
+ system: input.system,
228
+ model: input.model,
229
+ model_pref_key: input.modelPrefKey,
230
+ effort: input.effort
231
+ }),
232
+ embed: (input) => brokerCall(transport, "engines", "embed", input)
233
+ },
234
+ tts: {
235
+ speak: (input) => transport.direct("/api/voice/speak", input)
236
+ },
237
+ stt: {
238
+ transcribe: (input) => transport.direct("/api/voice/transcribe", input)
239
+ },
240
+ image: {
241
+ generate: (input) => transport.direct("/api/images/generate", input)
242
+ }
243
+ };
244
+ }
245
+
246
+ export {
247
+ httpPrimitiveTransport,
248
+ PRIMITIVE_BINDINGS,
249
+ createPrimitives
250
+ };
@@ -0,0 +1,200 @@
1
+ import {
2
+ MCP_PROTOCOL_VERSION,
3
+ callTool,
4
+ listTools
5
+ } from "./chunk-VLIRNNAE.js";
6
+
7
+ // src/mcp/server.ts
8
+ import { createInterface } from "readline";
9
+ function respond(id, result) {
10
+ return { jsonrpc: "2.0", id: id ?? null, result };
11
+ }
12
+ function respondError(id, code, message) {
13
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
14
+ }
15
+ function wrapContent(value) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: typeof value === "string" ? value : JSON.stringify(value)
21
+ }
22
+ ]
23
+ };
24
+ }
25
+ function unwrapContent(raw) {
26
+ const r = raw;
27
+ const text = r?.content?.[0]?.text;
28
+ if (text === void 0) {
29
+ return raw;
30
+ }
31
+ try {
32
+ return JSON.parse(text);
33
+ } catch {
34
+ return text;
35
+ }
36
+ }
37
+ var McpServer = class {
38
+ runnables = /* @__PURE__ */ new Map();
39
+ passthroughs = [];
40
+ /**
41
+ * Register an SDK Runnable as an MCP tool.
42
+ * Returns `this` for chaining.
43
+ */
44
+ register(runnable) {
45
+ this.runnables.set(runnable.name, runnable);
46
+ return this;
47
+ }
48
+ /**
49
+ * Register a passthrough to an external MCP stdio server (e.g. Ghost or
50
+ * Shadow). Tools from that server are fetched lazily and re-advertised.
51
+ * Returns `this` for chaining.
52
+ */
53
+ passthrough(registration) {
54
+ this.passthroughs.push(registration);
55
+ return this;
56
+ }
57
+ /** Fetch all tools: local Runnables + passthrough tools. */
58
+ async allTools() {
59
+ const local = [...this.runnables.values()].map((r) => ({
60
+ name: r.name,
61
+ description: r.description,
62
+ inputSchema: r.inputSchema
63
+ }));
64
+ const remote = [];
65
+ for (const pt of this.passthroughs) {
66
+ try {
67
+ const tools = await listTools(pt.command);
68
+ remote.push(...tools);
69
+ } catch (err) {
70
+ process.stderr.write(
71
+ `[mcp-server] passthrough '${pt.label}' list_tools failed: ${err}
72
+ `
73
+ );
74
+ }
75
+ }
76
+ return [...local, ...remote];
77
+ }
78
+ /** Handle a `tools/call` request. */
79
+ async handleCallTool(name, args) {
80
+ const local = this.runnables.get(name);
81
+ if (local) {
82
+ const result = await local.run(args);
83
+ return wrapContent(result);
84
+ }
85
+ for (const pt of this.passthroughs) {
86
+ try {
87
+ const tools = await listTools(pt.command);
88
+ if (tools.some((t) => t.name === name)) {
89
+ return await callTool(pt.command, name, args);
90
+ }
91
+ } catch {
92
+ }
93
+ }
94
+ throw new Error(`Unknown tool: ${name}`);
95
+ }
96
+ /**
97
+ * Handle a single parsed JSON-RPC request line. Returns the response to write
98
+ * (or null for notifications that require no response).
99
+ */
100
+ async handleRequest(req, initialized, write) {
101
+ const { id, method, params } = req;
102
+ if (method === "initialize") {
103
+ initialized.value = true;
104
+ write(
105
+ respond(id, {
106
+ protocolVersion: MCP_PROTOCOL_VERSION,
107
+ capabilities: { tools: {} },
108
+ serverInfo: { name: "ryu-sdk-server", version: "0.0.1" }
109
+ })
110
+ );
111
+ return;
112
+ }
113
+ if (method === "notifications/initialized") {
114
+ return;
115
+ }
116
+ if (!initialized.value) {
117
+ write(respondError(id, -32002, "Server not initialized"));
118
+ return;
119
+ }
120
+ if (method === "tools/list") {
121
+ try {
122
+ const tools = await this.allTools();
123
+ write(respond(id, { tools }));
124
+ } catch (err) {
125
+ write(respondError(id, -32603, String(err)));
126
+ }
127
+ return;
128
+ }
129
+ if (method === "tools/call") {
130
+ await this.handleToolsCall(id, params, write);
131
+ return;
132
+ }
133
+ write(respondError(id, -32601, `Method not found: ${method}`));
134
+ }
135
+ /** Handle a `tools/call` JSON-RPC request. */
136
+ async handleToolsCall(id, params, write) {
137
+ const p = params;
138
+ const toolName = p?.name;
139
+ const toolArgs = p?.arguments ?? {};
140
+ if (typeof toolName !== "string") {
141
+ write(respondError(id, -32602, "tools/call requires 'name'"));
142
+ return;
143
+ }
144
+ try {
145
+ const result = await this.handleCallTool(toolName, toolArgs);
146
+ write(respond(id, result));
147
+ } catch (err) {
148
+ write(
149
+ respond(id, {
150
+ content: [{ type: "text", text: String(err) }],
151
+ isError: true
152
+ })
153
+ );
154
+ }
155
+ }
156
+ /**
157
+ * Start reading JSON-RPC requests from the given readable stream and writing
158
+ * responses to the given writable stream.
159
+ *
160
+ * Defaults to `process.stdin` / `process.stdout`. Passing explicit streams
161
+ * lets tests inject a pair of in-process pipes.
162
+ *
163
+ * Resolves when the input stream ends (EOF).
164
+ */
165
+ serve(input = process.stdin, output = process.stdout) {
166
+ const write = (obj) => {
167
+ output.write(`${JSON.stringify(obj)}
168
+ `);
169
+ };
170
+ const initialized = { value: false };
171
+ return new Promise((resolve) => {
172
+ const rl = createInterface({
173
+ input,
174
+ crlfDelay: Number.POSITIVE_INFINITY
175
+ });
176
+ rl.on("line", (rawLine) => {
177
+ const line = rawLine.trim();
178
+ if (!line) {
179
+ return;
180
+ }
181
+ let req;
182
+ try {
183
+ req = JSON.parse(line);
184
+ } catch {
185
+ write(respondError(null, -32700, "Parse error"));
186
+ return;
187
+ }
188
+ this.handleRequest(req, initialized, write).catch((err) => {
189
+ write(respondError(req.id, -32603, String(err)));
190
+ });
191
+ });
192
+ rl.on("close", resolve);
193
+ });
194
+ }
195
+ };
196
+
197
+ export {
198
+ unwrapContent,
199
+ McpServer
200
+ };
@@ -77,24 +77,7 @@ function defineModel(modelId, options = {}) {
77
77
  return new ModelClient2(modelId, options);
78
78
  }
79
79
 
80
- // src/model/gateway.ts
81
- import * as native2 from "@ryuhq/sdk-native";
82
- var DEFAULT_GATEWAY_URL = "http://127.0.0.1:7981";
83
- function resolveGatewayUrl3() {
84
- return native2.resolveGatewayUrl();
85
- }
86
- function resolveGatewayToken2() {
87
- return native2.resolveGatewayToken() ?? void 0;
88
- }
89
- function assertAllowedEgressUrl(baseUrl) {
90
- native2.assertAllowedEgress(baseUrl);
91
- }
92
-
93
80
  export {
94
81
  ModelClient2 as ModelClient,
95
- defineModel,
96
- DEFAULT_GATEWAY_URL,
97
- resolveGatewayUrl3 as resolveGatewayUrl,
98
- resolveGatewayToken2 as resolveGatewayToken,
99
- assertAllowedEgressUrl
82
+ defineModel
100
83
  };
@@ -146,8 +146,30 @@ var WidgetContributionSchema = z.object({
146
146
  /** Default display mode (`inline` | `fullscreen` | `pip`). */
147
147
  default_display_mode: z.string().default(DEFAULT_WIDGET_DISPLAY_MODE)
148
148
  });
149
+ var ChatWidgetTemplateSchema = z.object({
150
+ id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/),
151
+ title: z.string().min(1),
152
+ description: z.string().optional(),
153
+ triggers: z.array(z.string()).default([]),
154
+ examples: z.array(z.string()).default([]),
155
+ backing: z.object({
156
+ tool_id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/).optional(),
157
+ view_id: z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/).optional()
158
+ }),
159
+ display_mode: z.string().min(1),
160
+ safe_action_ids: z.array(z.string().regex(/^[a-z0-9][a-z0-9._-]*$/)).default([]),
161
+ availability: z.string().default("available")
162
+ }).superRefine((value, ctx) => {
163
+ const count = Number(Boolean(value.backing.tool_id)) + Number(Boolean(value.backing.view_id));
164
+ if (count !== 1 && value.availability === "available") {
165
+ ctx.addIssue({ code: "custom", path: ["backing"], message: "available templates need exactly one backing tool_id or view_id" });
166
+ }
167
+ if (count > 1) {
168
+ ctx.addIssue({ code: "custom", path: ["backing"], message: "backing must declare at most one of tool_id or view_id" });
169
+ }
170
+ });
149
171
  var ToolAppConfigSchema = z.object({
150
- /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
172
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
151
173
  slug: z.string().min(1),
152
174
  /** The tool description the model reads when choosing it. Carried here because a
153
175
  * packed app's manifest is the only channel (there is no `generated.rs`); Core's
@@ -174,6 +196,11 @@ var ContributesSchema = z.object({
174
196
  * signing, leaving an app that emits events nothing is allowed to subscribe to. */
175
197
  hook_events: z.array(HookEventContributionSchema).default([]),
176
198
  composer_controls: z.array(z.record(z.string(), z.unknown())).default([]),
199
+ /** Chat feature descriptors whose behavior is implemented by the host shell.
200
+ * Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
201
+ * authoring schema prevents `ryu pack` from silently deleting a plugin's chat
202
+ * feature declaration before signing. */
203
+ chat_features: z.array(z.record(z.string(), z.unknown())).default([]),
177
204
  settings_tabs: z.array(z.record(z.string(), z.unknown())).default([]),
178
205
  slash_commands: z.array(z.record(z.string(), z.unknown())).default([]),
179
206
  /** App widgets (Ryu Apps). Each binds a render tool id to its
@@ -181,6 +208,8 @@ var ContributesSchema = z.object({
181
208
  * `Contributes.widgets` field, without which the CLI's zod parse would strip
182
209
  * every widget an app authored here declares. */
183
210
  widgets: z.array(WidgetContributionSchema).default([]),
211
+ /** Metadata-only chat widget templates. */
212
+ chat_widget_templates: z.array(ChatWidgetTemplateSchema).optional(),
184
213
  /** App-registered sidebar sections (header + live list) and buttons (single nav
185
214
  * rows). Loosely typed here — the shell owns the spec vocabulary — matching how
186
215
  * `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
@@ -193,6 +222,13 @@ var ContributesSchema = z.object({
193
222
  * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
194
223
  * strip the dock panel an app declares here. */
195
224
  dock_panels: z.array(z.record(z.string(), z.unknown())).default([]),
225
+ /** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
226
+ * typed for the same reason as the surfaces above — the shell owns the
227
+ * `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
228
+ * without it the CLI's zod parse would strip every live activity an app
229
+ * declares here, so a packed bundle would ship a dock that silently stays
230
+ * empty. */
231
+ live_activities: z.array(z.record(z.string(), z.unknown())).default([]),
196
232
  /** Deletable data categories the app owns — one "Delete all X" row in Settings
197
233
  * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
198
234
  * it the CLI's zod parse would strip the declaration before signing, and the
@@ -230,7 +266,10 @@ var ContributesSchema = z.object({
230
266
  * bug: the styles' `.md` files are not carried by the bundle either, so there
231
267
  * would be no residue to notice — the plugin would install clean and contribute
232
268
  * nothing. */
233
- output_styles: z.array(OutputStyleContributionSchema).default([])
269
+ output_styles: z.array(OutputStyleContributionSchema).default([]),
270
+ /** Per-message actions contributed by an enabled plugin. Kept as loose records
271
+ * so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
272
+ message_actions: z.array(z.record(z.string(), z.unknown())).default([])
234
273
  });
235
274
  var SetupStepSchema = z.object({
236
275
  /** Card heading (e.g. the companion app name). */
@@ -311,6 +350,61 @@ var EnginesReqSchema = z.object({
311
350
  /** Floor for the Next.js web app. */
312
351
  web: z.string().optional()
313
352
  });
353
+ var McpServerAuthSchema = z.object({
354
+ client_id: z.string().min(1).optional(),
355
+ type: z.literal("oauth")
356
+ }).strict();
357
+ var McpServerDeclSchema = z.object({
358
+ args: z.array(z.string()).default([]),
359
+ auth: McpServerAuthSchema.optional(),
360
+ command: z.string().optional(),
361
+ command_env: z.string().optional(),
362
+ description: z.string().optional(),
363
+ enabled: z.boolean().default(true),
364
+ env: z.record(z.string(), z.string()).default({}),
365
+ headers: z.record(z.string(), z.string()).default({}),
366
+ type: z.enum(["stdio", "http", "streamable-http", "streamable_http", "sse"]).optional(),
367
+ url: z.url().optional()
368
+ }).superRefine((server, context) => {
369
+ if (!(server.command || server.url)) {
370
+ context.addIssue({
371
+ code: "custom",
372
+ message: "an MCP server requires command or url"
373
+ });
374
+ }
375
+ if (!server.auth) {
376
+ return;
377
+ }
378
+ if (server.command || server.type === "stdio" || !server.url) {
379
+ context.addIssue({
380
+ code: "custom",
381
+ message: "OAuth is supported only for remote HTTP MCP servers"
382
+ });
383
+ return;
384
+ }
385
+ if (Object.keys(server.headers).some(
386
+ (name) => name.toLowerCase() === "authorization"
387
+ )) {
388
+ context.addIssue({
389
+ code: "custom",
390
+ message: "OAuth cannot be combined with a static Authorization header"
391
+ });
392
+ }
393
+ const url = new URL(server.url);
394
+ const loopback = url.hostname === "localhost" || url.hostname.startsWith("127.") || url.hostname === "[::1]" || url.hostname === "::1";
395
+ if (url.username || url.password || url.hash) {
396
+ context.addIssue({
397
+ code: "custom",
398
+ message: "OAuth MCP URLs cannot contain credentials or fragments"
399
+ });
400
+ }
401
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
402
+ context.addIssue({
403
+ code: "custom",
404
+ message: "OAuth MCP URLs must use HTTPS except on loopback"
405
+ });
406
+ }
407
+ });
314
408
  var PluginManifestSchema = z.object({
315
409
  /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
316
410
  id: z.string().min(1, "id is required"),
@@ -342,6 +436,8 @@ var PluginManifestSchema = z.object({
342
436
  * Declarations only — grant enforcement is the Gateway's responsibility.
343
437
  */
344
438
  permission_grants: z.array(z.string()).default([]),
439
+ /** Remote or stdio MCP servers registered by this plugin. */
440
+ mcp_servers: z.record(z.string(), McpServerDeclSchema).optional(),
345
441
  /**
346
442
  * Optional Companion surface (an in-desktop overlay or sidebar panel).
347
443
  * Absent when the plugin has no Companion surface.
@@ -435,10 +531,16 @@ var PluginManifestSchema = z.object({
435
531
  url: z.string().optional()
436
532
  })
437
533
  ]).optional(),
534
+ /** Public source repository URL (Claude/Codex `repository`). */
535
+ repository: z.string().url().optional(),
536
+ /** True when the provider operates outside the local Ryu runtime. */
537
+ external: z.boolean().optional(),
438
538
  /** Project/marketing homepage — maps to the listing `website` (Claude field). */
439
539
  homepage: z.string().optional(),
440
540
  /** Free-text search keywords (Claude field). */
441
541
  keywords: z.array(z.string()).optional(),
542
+ /** Stable Marketplace filter labels (Ryu extension). */
543
+ tags: z.array(z.string()).optional(),
442
544
  /** Taxonomy category beyond the runnable kinds (Claude field). */
443
545
  category: z.string().optional(),
444
546
  /** SPDX-ish license identifier (Claude field). */
@@ -452,6 +554,8 @@ var PluginManifestSchema = z.object({
452
554
  * from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
453
555
  */
454
556
  icon: z.string().optional(),
557
+ /** Optional detail-page hero banner metadata forwarded to the marketplace. */
558
+ banner: z.record(z.string(), z.unknown()).optional(),
455
559
  /**
456
560
  * Dithered-gradient background for the card's icon square (Ryu extension),
457
561
  * mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
@@ -483,6 +587,22 @@ var PluginManifestSchema = z.object({
483
587
  * array of steps guiding the user through post-install configuration.
484
588
  */
485
589
  setup: z.union([SetupStepSchema, z.array(SetupStepSchema)]).optional()
590
+ }).superRefine((manifest, context) => {
591
+ const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
592
+ (server) => server.auth?.type === "oauth"
593
+ );
594
+ if (!hasOAuthServer) {
595
+ return;
596
+ }
597
+ for (const grant of ["mcp:server", "identity.read"]) {
598
+ if (!manifest.permission_grants.includes(grant)) {
599
+ context.addIssue({
600
+ code: "custom",
601
+ message: `OAuth MCP servers require the ${grant} permission grant`,
602
+ path: ["permission_grants"]
603
+ });
604
+ }
605
+ }
486
606
  });
487
607
  function validatePluginId(id) {
488
608
  loadNative().validatePluginId(id);
@@ -504,6 +624,7 @@ export {
504
624
  PiExtensionContributionSchema,
505
625
  OutputStyleContributionSchema,
506
626
  WidgetContributionSchema,
627
+ ChatWidgetTemplateSchema,
507
628
  ToolAppConfigSchema,
508
629
  ContributesSchema,
509
630
  SetupStepSchema,
@@ -512,6 +633,8 @@ export {
512
633
  RequiresSchema,
513
634
  SurfaceSchema,
514
635
  EnginesReqSchema,
636
+ McpServerAuthSchema,
637
+ McpServerDeclSchema,
515
638
  PluginManifestSchema,
516
639
  validatePluginId,
517
640
  validateManifestStrict,