@ryuhq/sdk 0.0.5

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 (61) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +31 -0
  3. package/dist/agent.cjs +761 -0
  4. package/dist/agent.d.cts +3 -0
  5. package/dist/agent.d.ts +3 -0
  6. package/dist/agent.js +23 -0
  7. package/dist/chunk-GXHL5CO7.js +353 -0
  8. package/dist/chunk-KPKMMGVC.js +671 -0
  9. package/dist/chunk-ODFEUVPW.js +100 -0
  10. package/dist/cli.cjs +858 -0
  11. package/dist/cli.d.cts +1 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +454 -0
  14. package/dist/index-CEbS1SlS.d.cts +988 -0
  15. package/dist/index-DAxq7Y0R.d.ts +988 -0
  16. package/dist/index.cjs +1900 -0
  17. package/dist/index.d.cts +759 -0
  18. package/dist/index.d.ts +759 -0
  19. package/dist/index.js +771 -0
  20. package/dist/manifest.cjs +399 -0
  21. package/dist/manifest.d.cts +355 -0
  22. package/dist/manifest.d.ts +355 -0
  23. package/dist/manifest.js +38 -0
  24. package/package.json +56 -0
  25. package/src/agent/agent.ts +208 -0
  26. package/src/agent/index.ts +51 -0
  27. package/src/agent/loop.test.ts +261 -0
  28. package/src/agent/loop.ts +259 -0
  29. package/src/agent/model-call.ts +190 -0
  30. package/src/agent/query.ts +40 -0
  31. package/src/agent/tools.ts +295 -0
  32. package/src/builder.ts +473 -0
  33. package/src/cli/dev.test.ts +178 -0
  34. package/src/cli/dev.ts +425 -0
  35. package/src/cli.ts +390 -0
  36. package/src/contracts-lockstep.test.ts +77 -0
  37. package/src/generated/plugin-manifest.ts +1121 -0
  38. package/src/index.ts +141 -0
  39. package/src/manifest.test.ts +610 -0
  40. package/src/manifest.ts +589 -0
  41. package/src/mcp/bridge.test.ts +196 -0
  42. package/src/mcp/client.ts +253 -0
  43. package/src/mcp/fixture-server.ts +23 -0
  44. package/src/mcp/server.ts +351 -0
  45. package/src/model/client.test.ts +107 -0
  46. package/src/model/client.ts +179 -0
  47. package/src/model/gateway.ts +41 -0
  48. package/src/plugin/ryu-plugin.ts +191 -0
  49. package/src/runnable/agent.ts +338 -0
  50. package/src/runnable/app.ts +233 -0
  51. package/src/runnable/index.ts +61 -0
  52. package/src/runnable/primitives-hostapi.test.ts +73 -0
  53. package/src/runnable/primitives.test.ts +286 -0
  54. package/src/runnable/primitives.ts +610 -0
  55. package/src/runnable/runnable-types.ts +113 -0
  56. package/src/runnable/runnable.test.ts +397 -0
  57. package/src/runnable/skill.ts +60 -0
  58. package/src/runnable/tool.ts +260 -0
  59. package/src/runnable/turn-hook.test.ts +81 -0
  60. package/src/runnable/turn-hook.ts +191 -0
  61. package/src/runnable/workflow.ts +76 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1900 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ Agent: () => Agent,
34
+ AgentBuilder: () => AgentBuilder,
35
+ AppBuilder: () => AppBuilder,
36
+ AppDependencySchema: () => AppDependencySchema,
37
+ CapabilityReqSchema: () => CapabilityReqSchema,
38
+ CompanionSurfaceSchema: () => CompanionSurfaceSchema,
39
+ DEFAULT_GATEWAY_URL: () => DEFAULT_GATEWAY_URL,
40
+ ModelClient: () => ModelClient2,
41
+ PRIMITIVE_BINDINGS: () => PRIMITIVE_BINDINGS,
42
+ PluginBuilder: () => PluginBuilder,
43
+ PluginManifestSchema: () => PluginManifestSchema,
44
+ RequiresSchema: () => RequiresSchema,
45
+ RunnableKindSchema: () => RunnableKindSchema,
46
+ RunnableMetaSchema: () => RunnableMetaSchema,
47
+ SkillBuilder: () => SkillBuilder,
48
+ SurfaceSchema: () => SurfaceSchema,
49
+ ToolAppConfigSchema: () => ToolAppConfigSchema,
50
+ ToolBuilder: () => ToolBuilder,
51
+ WidgetContributionSchema: () => WidgetContributionSchema,
52
+ WorkflowBuilder: () => WorkflowBuilder,
53
+ agent: () => agent,
54
+ app: () => app,
55
+ appToolId: () => appToolId,
56
+ assertAllowedEgressUrl: () => assertAllowedEgressUrl,
57
+ coreManifestJsonSchema: () => coreManifestJsonSchema,
58
+ createAgent: () => createAgent,
59
+ createPrimitives: () => createPrimitives,
60
+ defineAgent: () => defineAgent,
61
+ defineApp: () => defineApp,
62
+ defineModel: () => defineModel,
63
+ definePlugin: () => definePlugin,
64
+ defineSkill: () => defineSkill,
65
+ defineTool: () => defineTool,
66
+ defineTurnHook: () => defineTurnHook,
67
+ defineWorkflow: () => defineWorkflow,
68
+ httpPrimitiveTransport: () => httpPrimitiveTransport,
69
+ inlineToolRunnable: () => inlineToolRunnable,
70
+ query: () => query,
71
+ resolveGatewayToken: () => resolveGatewayToken2,
72
+ resolveGatewayUrl: () => resolveGatewayUrl3,
73
+ ryuTool: () => ryuTool,
74
+ skill: () => skill,
75
+ tool: () => tool,
76
+ validateManifestStrict: () => validateManifestStrict,
77
+ validatePluginId: () => validatePluginId,
78
+ workflow: () => workflow
79
+ });
80
+ module.exports = __toCommonJS(src_exports);
81
+
82
+ // ../../node_modules/.bun/tsup@8.5.1+7a7b737fa55947b7/node_modules/tsup/assets/cjs_shims.js
83
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
84
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
85
+
86
+ // src/model/client.ts
87
+ var native = __toESM(require("@ryuhq/sdk-native"), 1);
88
+ var ModelClient2 = class {
89
+ model;
90
+ baseUrl;
91
+ native;
92
+ constructor(model, options = {}) {
93
+ this.native = new native.ModelClient(
94
+ model,
95
+ options.baseUrl ?? null,
96
+ options.token ?? null
97
+ );
98
+ this.model = model;
99
+ this.baseUrl = options.baseUrl ?? native.resolveGatewayUrl();
100
+ }
101
+ /** Send a non-streaming chat completion request to the gateway. */
102
+ async chat(messages) {
103
+ const res = await this.native.chat(messages);
104
+ const usage = res.promptTokens === void 0 && res.completionTokens === void 0 && res.totalTokens === void 0 ? void 0 : {
105
+ promptTokens: res.promptTokens ?? 0,
106
+ completionTokens: res.completionTokens ?? 0,
107
+ totalTokens: res.totalTokens ?? 0
108
+ };
109
+ return {
110
+ content: res.content,
111
+ finishReason: res.finishReason ?? null,
112
+ usage
113
+ };
114
+ }
115
+ /** Send a streaming chat completion request, yielding deltas as they arrive. */
116
+ async *stream(messages) {
117
+ const queue = [];
118
+ let done = false;
119
+ let failure = null;
120
+ let wake = null;
121
+ const notify = () => {
122
+ if (wake) {
123
+ const w = wake;
124
+ wake = null;
125
+ w();
126
+ }
127
+ };
128
+ this.native.stream(messages, (err, delta) => {
129
+ if (err) {
130
+ failure = err;
131
+ done = true;
132
+ } else if (delta === null || delta === void 0) {
133
+ done = true;
134
+ } else {
135
+ queue.push({
136
+ content: delta.content ?? null,
137
+ finishReason: delta.finishReason ?? null
138
+ });
139
+ }
140
+ notify();
141
+ });
142
+ while (true) {
143
+ while (queue.length > 0) {
144
+ const next = queue.shift();
145
+ if (next) {
146
+ yield next;
147
+ }
148
+ }
149
+ if (failure) {
150
+ throw failure;
151
+ }
152
+ if (done) {
153
+ return;
154
+ }
155
+ await new Promise((resolve) => {
156
+ wake = resolve;
157
+ });
158
+ }
159
+ }
160
+ };
161
+ function defineModel(modelId, options = {}) {
162
+ return new ModelClient2(modelId, options);
163
+ }
164
+
165
+ // src/model/gateway.ts
166
+ var native2 = __toESM(require("@ryuhq/sdk-native"), 1);
167
+ var DEFAULT_GATEWAY_URL = "http://127.0.0.1:7981";
168
+ function resolveGatewayUrl3() {
169
+ return native2.resolveGatewayUrl();
170
+ }
171
+ function resolveGatewayToken2() {
172
+ return native2.resolveGatewayToken() ?? void 0;
173
+ }
174
+ function assertAllowedEgressUrl(baseUrl) {
175
+ native2.assertAllowedEgress(baseUrl);
176
+ }
177
+
178
+ // src/runnable/primitives.ts
179
+ function dataUrlToBytes(dataUrl) {
180
+ const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
181
+ if (!match) {
182
+ throw new Error(
183
+ "stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
184
+ );
185
+ }
186
+ const mediaType = match[1] || "application/octet-stream";
187
+ const isBase64 = Boolean(match[2]);
188
+ const payload = match[3] ?? "";
189
+ if (isBase64) {
190
+ const binary = atob(payload);
191
+ const bytes = new Uint8Array(binary.length);
192
+ for (let i = 0; i < binary.length; i++) {
193
+ bytes[i] = binary.charCodeAt(i);
194
+ }
195
+ return { bytes, mediaType };
196
+ }
197
+ return { bytes: new TextEncoder().encode(decodeURIComponent(payload)), mediaType };
198
+ }
199
+ function bytesToDataUrl(bytes, mediaType) {
200
+ let binary = "";
201
+ const chunk = 32768;
202
+ for (let i = 0; i < bytes.length; i += chunk) {
203
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
204
+ }
205
+ return `data:${mediaType};base64,${btoa(binary)}`;
206
+ }
207
+ function httpPrimitiveTransport(options) {
208
+ const base = options.nodeUrl.replace(/\/+$/, "");
209
+ assertAllowedEgressUrl(base);
210
+ const doFetch = options.fetchImpl ?? fetch;
211
+ const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
212
+ const failDetail = async (path, res) => {
213
+ const detail = await res.text().catch(() => "");
214
+ return new Error(
215
+ `Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
216
+ );
217
+ };
218
+ const post = async (path, body) => {
219
+ const res = await doFetch(`${base}${path}`, {
220
+ method: "POST",
221
+ headers: { "content-type": "application/json", ...authHeader() },
222
+ body: JSON.stringify(body ?? {})
223
+ });
224
+ if (!res.ok) {
225
+ throw await failDetail(path, res);
226
+ }
227
+ const text = await res.text();
228
+ return text ? JSON.parse(text) : void 0;
229
+ };
230
+ const transcribeDirect = async (body) => {
231
+ const input = body ?? {};
232
+ if (!input.audio) {
233
+ throw new Error(
234
+ "stt.transcribe requires an `audio` data: URL (the recorded audio)"
235
+ );
236
+ }
237
+ const { bytes, mediaType } = dataUrlToBytes(input.audio);
238
+ const form = new FormData();
239
+ form.append(
240
+ "file",
241
+ new Blob([bytes], { type: mediaType || "audio/wav" }),
242
+ input.filename ?? "recording.wav"
243
+ );
244
+ const res = await doFetch(`${base}/api/voice/transcribe`, {
245
+ method: "POST",
246
+ headers: authHeader(),
247
+ body: form
248
+ });
249
+ if (!res.ok) {
250
+ throw await failDetail("/api/voice/transcribe", res);
251
+ }
252
+ const parsed = await res.json();
253
+ return (parsed.text ?? "").trim();
254
+ };
255
+ const speakDirect = async (body) => {
256
+ const res = await doFetch(`${base}/api/voice/speak`, {
257
+ method: "POST",
258
+ headers: { "content-type": "application/json", ...authHeader() },
259
+ body: JSON.stringify(body ?? {})
260
+ });
261
+ if (!res.ok) {
262
+ throw await failDetail("/api/voice/speak", res);
263
+ }
264
+ const bytes = new Uint8Array(await res.arrayBuffer());
265
+ const mediaType = res.headers.get("content-type") || "audio/wav";
266
+ return bytesToDataUrl(bytes, mediaType);
267
+ };
268
+ const generateImageDirect = async (body) => {
269
+ const res = await doFetch(`${base}/api/images/generate`, {
270
+ method: "POST",
271
+ headers: { "content-type": "application/json", ...authHeader() },
272
+ body: JSON.stringify(body ?? {})
273
+ });
274
+ if (!res.ok) {
275
+ throw await failDetail("/api/images/generate", res);
276
+ }
277
+ const parsed = await res.json();
278
+ return (parsed.data ?? []).map(
279
+ (item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
280
+ );
281
+ };
282
+ return {
283
+ bridge(method, args) {
284
+ if (!options.pluginId) {
285
+ return Promise.reject(
286
+ new Error(
287
+ `bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
288
+ )
289
+ );
290
+ }
291
+ return post(`/api/plugins/${options.pluginId}/host`, { method, args });
292
+ },
293
+ direct(path, body) {
294
+ if (path === "/api/voice/transcribe") {
295
+ return transcribeDirect(body);
296
+ }
297
+ if (path === "/api/voice/speak") {
298
+ return speakDirect(body);
299
+ }
300
+ if (path === "/api/images/generate") {
301
+ return generateImageDirect(body);
302
+ }
303
+ return post(path, body);
304
+ },
305
+ capability(cap, body) {
306
+ return post(`/api/host/capability/${cap}`, body);
307
+ }
308
+ };
309
+ }
310
+ var PRIMITIVE_BINDINGS = {
311
+ // Bridge families (existing `PluginHookBridge`).
312
+ "engines.complete": {
313
+ transport: "bridge",
314
+ method: "model.complete",
315
+ grant: "hook:side-model"
316
+ },
317
+ // Host-direct media data-path (the host holds the node token; returns data: URLs).
318
+ "image.generate": {
319
+ transport: "direct",
320
+ path: "/api/images/generate",
321
+ grant: "media:generate"
322
+ },
323
+ "tts.speak": {
324
+ transport: "direct",
325
+ path: "/api/voice/speak",
326
+ grant: "media:generate"
327
+ },
328
+ "stt.transcribe": {
329
+ transport: "direct",
330
+ path: "/api/voice/transcribe",
331
+ grant: "media:transcribe"
332
+ },
333
+ // Broker capabilities — no rpc family yet (@requires-grant).
334
+ "rag.retrieve": { transport: "broker", capability: "rag" },
335
+ "rag.embed": { transport: "broker", capability: "rag" },
336
+ "rag.rerank": { transport: "broker", capability: "rag" },
337
+ "memory.recall": { transport: "broker", capability: "memory" },
338
+ "memory.store": { transport: "broker", capability: "memory" },
339
+ "realtime.broadcast": { transport: "broker", capability: "realtime" },
340
+ "realtime.subscribe": { transport: "broker", capability: "realtime" },
341
+ "durable.checkpoint": { transport: "broker", capability: "durable" },
342
+ "durable.resume": { transport: "broker", capability: "durable" },
343
+ "engines.embed": { transport: "broker", capability: "engines" }
344
+ };
345
+ function brokerCall(transport, cap, op, input) {
346
+ return transport.capability(cap, { op, input });
347
+ }
348
+ function createPrimitives(transport) {
349
+ return {
350
+ rag: {
351
+ retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
352
+ embed: (input) => brokerCall(transport, "rag", "embed", input),
353
+ rerank: (input) => brokerCall(transport, "rag", "rerank", input)
354
+ },
355
+ memory: {
356
+ recall: (input) => brokerCall(transport, "memory", "recall", input),
357
+ store: (input) => brokerCall(transport, "memory", "store", input)
358
+ },
359
+ realtime: {
360
+ broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
361
+ () => void 0
362
+ ),
363
+ subscribe: (input) => brokerCall(
364
+ transport,
365
+ "realtime",
366
+ "subscribe",
367
+ input
368
+ )
369
+ },
370
+ durable: {
371
+ checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
372
+ resume: (input) => brokerCall(transport, "durable", "resume", input)
373
+ },
374
+ engines: {
375
+ // Bridge family: model.complete → host.sideModel (wire keys are snake_case).
376
+ complete: (input) => transport.bridge("model.complete", {
377
+ prompt: input.prompt,
378
+ system: input.system,
379
+ model: input.model,
380
+ model_pref_key: input.modelPrefKey,
381
+ effort: input.effort
382
+ }),
383
+ embed: (input) => brokerCall(transport, "engines", "embed", input)
384
+ },
385
+ tts: {
386
+ speak: (input) => transport.direct("/api/voice/speak", input)
387
+ },
388
+ stt: {
389
+ transcribe: (input) => transport.direct("/api/voice/transcribe", input)
390
+ },
391
+ image: {
392
+ generate: (input) => transport.direct("/api/images/generate", input)
393
+ }
394
+ };
395
+ }
396
+
397
+ // src/agent/model-call.ts
398
+ var CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
399
+ function normalizeBaseUrl(baseUrl) {
400
+ return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
401
+ }
402
+ async function callModelWithTools(options) {
403
+ const base = normalizeBaseUrl(options.baseUrl);
404
+ assertAllowedEgressUrl(base);
405
+ const body = {
406
+ model: options.model,
407
+ messages: options.messages
408
+ };
409
+ if (options.tools && options.tools.length > 0) {
410
+ body.tools = options.tools;
411
+ if (options.toolChoice) {
412
+ body.tool_choice = options.toolChoice;
413
+ }
414
+ }
415
+ const headers = {
416
+ "content-type": "application/json",
417
+ // Force the gateway's plain-completion branch so our own tool_calls are
418
+ // returned verbatim on Composio-on managed nodes.
419
+ "x-ryu-raw-tools": "on"
420
+ };
421
+ if (options.token) {
422
+ headers.authorization = `Bearer ${options.token}`;
423
+ }
424
+ const res = await fetch(`${base}${CHAT_COMPLETIONS_PATH}`, {
425
+ method: "POST",
426
+ headers,
427
+ body: JSON.stringify(body),
428
+ signal: options.signal
429
+ });
430
+ if (!res.ok) {
431
+ const text = await res.text().catch(() => "");
432
+ throw new Error(
433
+ `[ryu-sdk] gateway ${res.status} ${res.statusText} at ${base}${CHAT_COMPLETIONS_PATH}${text ? `: ${text}` : ""}`
434
+ );
435
+ }
436
+ const json = await res.json();
437
+ const choice = json.choices?.[0];
438
+ const rawMessage = choice?.message;
439
+ const message = {
440
+ role: "assistant",
441
+ content: rawMessage?.content ?? null,
442
+ ...rawMessage?.tool_calls && rawMessage.tool_calls.length > 0 ? { tool_calls: rawMessage.tool_calls } : {}
443
+ };
444
+ const usage = json.usage ? {
445
+ promptTokens: json.usage.prompt_tokens ?? 0,
446
+ completionTokens: json.usage.completion_tokens ?? 0,
447
+ totalTokens: json.usage.total_tokens ?? 0
448
+ } : void 0;
449
+ return {
450
+ message,
451
+ finishReason: choice?.finish_reason ?? null,
452
+ usage
453
+ };
454
+ }
455
+
456
+ // src/agent/tools.ts
457
+ function ryuTool(id, opts = {}) {
458
+ return {
459
+ kind: "remote",
460
+ id,
461
+ description: opts.description,
462
+ parameters: opts.parameters
463
+ };
464
+ }
465
+ function isLocalTool(tool2) {
466
+ return tool2.kind === "tool";
467
+ }
468
+ var PERMISSIVE_OBJECT_SCHEMA = {
469
+ type: "object",
470
+ additionalProperties: true
471
+ };
472
+ function normalize(baseUrl) {
473
+ return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
474
+ }
475
+ function authHeaders(token) {
476
+ const headers = {
477
+ "content-type": "application/json"
478
+ };
479
+ if (token) {
480
+ headers.authorization = `Bearer ${token}`;
481
+ }
482
+ return headers;
483
+ }
484
+ async function resolveToolDefs(tools, ctx) {
485
+ const defs = [];
486
+ for (const [name, tool2] of Object.entries(tools)) {
487
+ if (isLocalTool(tool2)) {
488
+ defs.push({
489
+ type: "function",
490
+ function: {
491
+ name,
492
+ description: tool2.name,
493
+ parameters: tool2.schema
494
+ }
495
+ });
496
+ continue;
497
+ }
498
+ let description = tool2.description;
499
+ if (!description) {
500
+ description = await describeRemoteTool(tool2.id, ctx);
501
+ }
502
+ defs.push({
503
+ type: "function",
504
+ function: {
505
+ name,
506
+ description: description ?? tool2.id,
507
+ parameters: tool2.parameters ?? PERMISSIVE_OBJECT_SCHEMA
508
+ }
509
+ });
510
+ }
511
+ return defs;
512
+ }
513
+ async function describeRemoteTool(id, ctx) {
514
+ const url = `${normalize(ctx.coreBaseUrl)}/api/tools/describe?id=${encodeURIComponent(id)}`;
515
+ try {
516
+ const res = await fetch(url, {
517
+ headers: authHeaders(ctx.coreToken),
518
+ signal: ctx.signal
519
+ });
520
+ if (!res.ok) {
521
+ return void 0;
522
+ }
523
+ const json = await res.json();
524
+ return json.description || json.name || void 0;
525
+ } catch {
526
+ return void 0;
527
+ }
528
+ }
529
+ async function executeTool(name, argsJson, tools, ctx) {
530
+ const tool2 = tools[name];
531
+ if (!tool2) {
532
+ throw new Error(`[ryu-sdk] model called unknown tool "${name}"`);
533
+ }
534
+ const args = parseArgs(argsJson, name);
535
+ if (isLocalTool(tool2)) {
536
+ const output = await tool2.run(
537
+ args,
538
+ ctx.runnableContext
539
+ );
540
+ return { output };
541
+ }
542
+ if (!ctx.agentId) {
543
+ throw new Error(
544
+ `[ryu-sdk] remote tool "${name}" (${tool2.id}) requires an agentId \u2014 set it on the Agent config so Core can govern the call`
545
+ );
546
+ }
547
+ const url = `${normalize(ctx.coreBaseUrl)}/api/mcp/tools/call`;
548
+ const res = await fetch(url, {
549
+ method: "POST",
550
+ headers: authHeaders(ctx.coreToken),
551
+ signal: ctx.signal,
552
+ body: JSON.stringify({
553
+ tool: tool2.id,
554
+ arguments: args,
555
+ agent_id: ctx.agentId,
556
+ ...ctx.userId ? { user_id: ctx.userId } : {}
557
+ })
558
+ });
559
+ if (!res.ok) {
560
+ const text = await res.text().catch(() => "");
561
+ throw new Error(
562
+ `[ryu-sdk] Core tools/call ${res.status} for "${tool2.id}"${text ? `: ${text}` : ""}`
563
+ );
564
+ }
565
+ const json = await res.json();
566
+ if (json.ok === false) {
567
+ throw new Error(
568
+ `[ryu-sdk] tool "${tool2.id}" failed: ${json.error ?? "unknown error"}`
569
+ );
570
+ }
571
+ return { output: json.output };
572
+ }
573
+ function parseArgs(argsJson, name) {
574
+ const trimmed = (argsJson ?? "").trim();
575
+ if (trimmed === "") {
576
+ return {};
577
+ }
578
+ try {
579
+ return JSON.parse(trimmed);
580
+ } catch {
581
+ throw new Error(
582
+ `[ryu-sdk] tool "${name}" arguments were not valid JSON: ${argsJson}`
583
+ );
584
+ }
585
+ }
586
+ var ELICITATION_KEY = "__ryu_elicitation__";
587
+ function detectElicitation(output) {
588
+ if (typeof output !== "object" || output === null) {
589
+ return null;
590
+ }
591
+ const envelope = output[ELICITATION_KEY];
592
+ if (typeof envelope !== "object" || envelope === null) {
593
+ return null;
594
+ }
595
+ return envelope;
596
+ }
597
+
598
+ // src/agent/loop.ts
599
+ function safeParse(json) {
600
+ try {
601
+ return JSON.parse(json);
602
+ } catch {
603
+ return json;
604
+ }
605
+ }
606
+ function addUsage(a, b) {
607
+ if (!(a || b)) {
608
+ return void 0;
609
+ }
610
+ return {
611
+ promptTokens: (a?.promptTokens ?? 0) + (b?.promptTokens ?? 0),
612
+ completionTokens: (a?.completionTokens ?? 0) + (b?.completionTokens ?? 0),
613
+ totalTokens: (a?.totalTokens ?? 0) + (b?.totalTokens ?? 0)
614
+ };
615
+ }
616
+ function findElicitation(output) {
617
+ const direct = detectElicitation(output);
618
+ if (direct) {
619
+ return direct;
620
+ }
621
+ if (typeof output === "string") {
622
+ return detectElicitation(safeParse(output));
623
+ }
624
+ return null;
625
+ }
626
+ async function* runAgentLoop(config) {
627
+ const messages = [...config.messages];
628
+ let toolDefs;
629
+ try {
630
+ toolDefs = await resolveToolDefs(config.tools, config.toolCtx);
631
+ } catch (err) {
632
+ yield { type: "error", message: describeError(err) };
633
+ return;
634
+ }
635
+ let usage;
636
+ let lastText = "";
637
+ for (let step = 1; step <= config.maxSteps; step++) {
638
+ let result;
639
+ try {
640
+ result = await callModelWithTools({
641
+ baseUrl: config.gatewayBaseUrl,
642
+ token: config.gatewayToken,
643
+ model: config.model,
644
+ messages,
645
+ tools: toolDefs.length > 0 ? toolDefs : void 0,
646
+ toolChoice: toolDefs.length > 0 ? "auto" : void 0,
647
+ signal: config.signal
648
+ });
649
+ } catch (err) {
650
+ yield { type: "error", message: describeError(err) };
651
+ return;
652
+ }
653
+ usage = addUsage(usage, result.usage);
654
+ const assistant = result.message;
655
+ messages.push(assistant);
656
+ if (assistant.content) {
657
+ lastText = assistant.content;
658
+ yield { type: "text", content: assistant.content };
659
+ }
660
+ const toolCalls = assistant.tool_calls ?? [];
661
+ if (toolCalls.length === 0) {
662
+ yield { type: "result", text: lastText, steps: step, usage };
663
+ return;
664
+ }
665
+ const paused = yield* runToolCalls(toolCalls, messages, config);
666
+ if (paused) {
667
+ return;
668
+ }
669
+ }
670
+ yield { type: "result", text: lastText, steps: config.maxSteps, usage };
671
+ }
672
+ async function* runToolCalls(toolCalls, messages, config) {
673
+ for (const call of toolCalls) {
674
+ const name = call.function.name;
675
+ const input = safeParse(call.function.arguments);
676
+ yield { type: "tool_call", id: call.id, name, input };
677
+ let output;
678
+ try {
679
+ const res = await executeTool(
680
+ name,
681
+ call.function.arguments,
682
+ config.tools,
683
+ config.toolCtx
684
+ );
685
+ output = res.output;
686
+ } catch (err) {
687
+ const errPayload = { error: describeError(err) };
688
+ messages.push({
689
+ role: "tool",
690
+ tool_call_id: call.id,
691
+ content: JSON.stringify(errPayload)
692
+ });
693
+ yield { type: "tool_result", id: call.id, name, output: errPayload };
694
+ continue;
695
+ }
696
+ const elicitation = findElicitation(output);
697
+ if (elicitation) {
698
+ yield {
699
+ type: "auth_required",
700
+ tool: name,
701
+ url: elicitation.url,
702
+ message: elicitation.message
703
+ };
704
+ return true;
705
+ }
706
+ messages.push({
707
+ role: "tool",
708
+ tool_call_id: call.id,
709
+ content: typeof output === "string" ? output : JSON.stringify(output)
710
+ });
711
+ yield { type: "tool_result", id: call.id, name, output };
712
+ }
713
+ return false;
714
+ }
715
+ function describeError(err) {
716
+ return err instanceof Error ? err.message : String(err);
717
+ }
718
+
719
+ // src/agent/agent.ts
720
+ var DEFAULT_MAX_STEPS = 10;
721
+ var DEFAULT_CORE_URL = "http://127.0.0.1:7980";
722
+ function env(key) {
723
+ const value = globalThis.process?.env?.[key];
724
+ return value && value !== "" ? value : void 0;
725
+ }
726
+ function lazyGatewayClient(model, baseUrl, token) {
727
+ let client = null;
728
+ const get = () => {
729
+ client ??= defineModel(model, { baseUrl, token });
730
+ return client;
731
+ };
732
+ return {
733
+ chat: (messages) => get().chat(messages),
734
+ stream: (messages) => get().stream(messages)
735
+ };
736
+ }
737
+ var Agent = class {
738
+ config;
739
+ constructor(config) {
740
+ this.config = config;
741
+ }
742
+ /** Assemble the loop config for a given prompt. */
743
+ buildLoopConfig(prompt, signal) {
744
+ const gatewayBaseUrl = this.config.node?.baseUrl ?? resolveGatewayUrl3();
745
+ const gatewayToken = this.config.node?.token ?? resolveGatewayToken2();
746
+ const coreBaseUrl = this.config.core?.baseUrl ?? env("RYU_CORE_URL") ?? DEFAULT_CORE_URL;
747
+ const coreToken = this.config.core?.token ?? env("RYU_TOKEN");
748
+ const messages = [];
749
+ if (this.config.instructions) {
750
+ messages.push({ role: "system", content: this.config.instructions });
751
+ }
752
+ messages.push({ role: "user", content: prompt });
753
+ const toolCtx = {
754
+ agentId: this.config.agentId,
755
+ coreBaseUrl,
756
+ coreToken,
757
+ userId: this.config.userId,
758
+ signal,
759
+ runnableContext: {
760
+ gateway: lazyGatewayClient(
761
+ this.config.model,
762
+ gatewayBaseUrl,
763
+ gatewayToken
764
+ ),
765
+ signal,
766
+ // Mount the composable primitive surface only when a plugin id is
767
+ // present — the bridge families authenticate it, so we never attach a
768
+ // half-wired transport (advisor guidance / §6b).
769
+ ...this.config.pluginId ? createPrimitives(
770
+ httpPrimitiveTransport({
771
+ nodeUrl: coreBaseUrl,
772
+ token: coreToken,
773
+ pluginId: this.config.pluginId
774
+ })
775
+ ) : {}
776
+ }
777
+ };
778
+ return {
779
+ model: this.config.model,
780
+ gatewayBaseUrl,
781
+ gatewayToken,
782
+ messages,
783
+ tools: this.config.tools ?? {},
784
+ toolCtx,
785
+ maxSteps: this.config.maxSteps ?? DEFAULT_MAX_STEPS,
786
+ signal
787
+ };
788
+ }
789
+ /** Stream loop events (text / tool_call / tool_result / auth_required / …). */
790
+ stream(prompt, signal) {
791
+ return runAgentLoop(this.buildLoopConfig(prompt, signal));
792
+ }
793
+ /** Run to completion and return the final text, step count, and usage. */
794
+ async generate(prompt, signal) {
795
+ let text = "";
796
+ let steps = 0;
797
+ let usage;
798
+ let authRequired;
799
+ for await (const event of this.stream(prompt, signal)) {
800
+ if (event.type === "result") {
801
+ text = event.text;
802
+ steps = event.steps;
803
+ usage = event.usage;
804
+ } else if (event.type === "auth_required") {
805
+ authRequired = event;
806
+ } else if (event.type === "error") {
807
+ throw new Error(
808
+ `[ryu-sdk] agent "${this.config.name}": ${event.message}`
809
+ );
810
+ }
811
+ }
812
+ return { text, steps, usage, authRequired };
813
+ }
814
+ };
815
+ function createAgent(config) {
816
+ return new Agent(config);
817
+ }
818
+
819
+ // src/agent/query.ts
820
+ function query(input) {
821
+ const agent2 = new Agent({
822
+ name: input.options.name ?? "agent",
823
+ ...input.options
824
+ });
825
+ return agent2.stream(input.prompt);
826
+ }
827
+
828
+ // src/manifest.ts
829
+ var import_node_module = require("module");
830
+ var import_zod = require("zod");
831
+ var cachedNative = null;
832
+ var nativeLoadError = null;
833
+ function loadNative() {
834
+ if (cachedNative) {
835
+ return cachedNative;
836
+ }
837
+ if (nativeLoadError) {
838
+ throw nativeLoadError;
839
+ }
840
+ try {
841
+ const req = (0, import_node_module.createRequire)(importMetaUrl);
842
+ cachedNative = req("@ryuhq/sdk-native");
843
+ return cachedNative;
844
+ } catch (cause) {
845
+ nativeLoadError = new Error(
846
+ "@ryuhq/sdk-native (the Rust-cored napi addon) is not available; Core-strict manifest validation requires it. Build/install the addon, or use PluginManifestSchema (pure-JS zod) for authoring-time validation.",
847
+ { cause }
848
+ );
849
+ throw nativeLoadError;
850
+ }
851
+ }
852
+ var RunnableKindSchema = import_zod.z.enum([
853
+ "agent",
854
+ "workflow",
855
+ "tool",
856
+ "skill",
857
+ // A companion surface (in-desktop panel). Added so a packable plugin can
858
+ // declare a companion runnable that flows through Core's existing Companion
859
+ // handler → app_contrib → `GET /api/plugins/contributions` → the desktop
860
+ // `/plugin/<id>` route. Its `config.ui_entry` (see `CompanionRunnableConfigSchema`)
861
+ // is what `ryu pack` bundles into `ui_code`.
862
+ "companion"
863
+ ]);
864
+ var RunnableMetaSchema = import_zod.z.object({
865
+ /** Stable unique identifier (e.g. `"agent-researcher"`). */
866
+ id: import_zod.z.string().min(1),
867
+ /** Human-readable display name. */
868
+ name: import_zod.z.string().min(1),
869
+ /** Which kind of runnable this entry describes. */
870
+ kind: RunnableKindSchema,
871
+ /**
872
+ * Optional per-kind config blob. Mirrors Core's `RunnableEntry.config`
873
+ * (`Option<serde_json::Value>`) so a manifest authored here round-trips
874
+ * through Core-strict validation. Left opaque (a record) at this authoring
875
+ * layer; the per-kind shape is enforced by Core's `validate_runnable`.
876
+ *
877
+ * For a `companion` runnable, `config.ui_entry` names the plugin's UI entry
878
+ * module (relative to the manifest dir). `ryu pack` bundles that entry into
879
+ * the emitted `ui_code`; Core's `CompanionConfig.ui_entry` is the lockstep
880
+ * field so a packed companion validates.
881
+ */
882
+ config: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
883
+ });
884
+ function labelImpersonatesSystemChrome(label) {
885
+ const lower = label.toLowerCase();
886
+ return lower.includes("ryu") || lower.includes("system");
887
+ }
888
+ var CompanionSurfaceSchema = import_zod.z.object({
889
+ /** Display label for the companion panel tab or tooltip. Anti-impersonation:
890
+ * may not pose as first-party Ryu/system chrome (see
891
+ * {@link labelImpersonatesSystemChrome}). */
892
+ label: import_zod.z.string().min(1).refine((value) => !labelImpersonatesSystemChrome(value), {
893
+ message: "companion label must not impersonate system chrome (must not contain 'ryu' or 'system')"
894
+ }),
895
+ /** Icon identifier resolved by the desktop shell. */
896
+ icon: import_zod.z.string().optional(),
897
+ /** Keyboard shortcut string (e.g. `"ctrl+shift+r"`). */
898
+ shortcut: import_zod.z.string().optional()
899
+ });
900
+ var TurnHookContributionSchema = import_zod.z.object({
901
+ /** Stable id for this hook (unique within the plugin). */
902
+ id: import_zod.z.string().min(1),
903
+ /** Turn boundary this fires on. Today only `"post_assistant_turn"`. */
904
+ on: import_zod.z.string().min(1).default("post_assistant_turn"),
905
+ /** The JS hook body executed in the sandbox (returns a directive). */
906
+ code: import_zod.z.string().min(1)
907
+ });
908
+ var DEFAULT_WIDGET_MIME = "text/html+skybridge";
909
+ var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
910
+ var WidgetContributionSchema = import_zod.z.object({
911
+ /** The fully-qualified tool id whose result renders this widget. */
912
+ tool_id: import_zod.z.string().min(1),
913
+ /** `ui://widget/<slug>.html` — the widget resource uri. */
914
+ uri: import_zod.z.string().min(1),
915
+ /** Source entry (e.g. `src/apps/checklist/index.tsx`) for `ryu pack`. */
916
+ ui_entry: import_zod.z.string().optional(),
917
+ /** Widget MIME dialect (default `text/html+skybridge`). */
918
+ mime: import_zod.z.string().default(DEFAULT_WIDGET_MIME),
919
+ /** Default display mode (`inline` | `fullscreen` | `pip`). */
920
+ default_display_mode: import_zod.z.string().default(DEFAULT_WIDGET_DISPLAY_MODE)
921
+ });
922
+ var ToolAppConfigSchema = import_zod.z.object({
923
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
924
+ slug: import_zod.z.string().min(1),
925
+ /** The tool description the model reads when choosing it. Carried here because a
926
+ * packed app's manifest is the only channel (there is no `generated.rs`); Core's
927
+ * app-tool synthesis reads it back onto the `RegistryTool`. */
928
+ description: import_zod.z.string(),
929
+ /** JSON Schema for the tool's arguments (used for validation + the LLM tool
930
+ * surface). Snake_case to match `widget_accessible`. Absent = no arguments. */
931
+ input_schema: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
932
+ /** True when calling this tool renders the app's widget (carries the template). */
933
+ widget: import_zod.z.boolean().default(false),
934
+ /** True when a mounted widget may `callTool` this tool (a companion), or when a
935
+ * render tool's widget may call any companion the app declares. */
936
+ widget_accessible: import_zod.z.boolean().default(false),
937
+ /** Optional status label shown while the render tool runs. */
938
+ invoking: import_zod.z.string().optional(),
939
+ /** Optional status label shown when the render tool finishes. */
940
+ invoked: import_zod.z.string().optional()
941
+ });
942
+ var ContributesSchema = import_zod.z.object({
943
+ turn_hooks: import_zod.z.array(TurnHookContributionSchema).default([]),
944
+ composer_controls: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
945
+ settings_tabs: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
946
+ slash_commands: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
947
+ /** App widgets (Ryu Apps). Each binds a render tool id to its
948
+ * `ui://widget/<slug>.html` template. Mirrors the Rust-side
949
+ * `Contributes.widgets` field, without which the CLI's zod parse would strip
950
+ * every widget an app authored here declares. */
951
+ widgets: import_zod.z.array(WidgetContributionSchema).default([])
952
+ });
953
+ var SetupStepSchema = import_zod.z.object({
954
+ /** Card heading (e.g. the companion app name). */
955
+ title: import_zod.z.string().optional(),
956
+ /** Instruction body shown under the title. */
957
+ description: import_zod.z.string().optional(),
958
+ /** Label for the optional action button. */
959
+ actionLabel: import_zod.z.string().optional(),
960
+ /** URL the action button opens (validated server-side on publish). */
961
+ actionUrl: import_zod.z.string().optional()
962
+ });
963
+ var AppDependencySchema = import_zod.z.object({
964
+ /** The `id` of the plugin this one depends on. */
965
+ id: import_zod.z.string().min(1, "dependency id is required"),
966
+ /** Optional MINIMUM version the dependency must satisfy (`"1.2.0"` = `">=1.2.0"`). */
967
+ min_version: import_zod.z.string().min(1).optional()
968
+ });
969
+ var CapabilityReqSchema = import_zod.z.object({
970
+ /** Capability name (e.g. `"rag"`, `"memory"`, `"tts"`). Matched against a
971
+ * provider's `provides[].capability`. */
972
+ capability: import_zod.z.string().min(1, "capability name is required"),
973
+ /** Optional MINIMUM capability version the bound provider must satisfy
974
+ * (`"1.2.0"` = `">=1.2.0"`). Absent = any version. */
975
+ min_version: import_zod.z.string().min(1).optional()
976
+ });
977
+ var RequiresSchema = import_zod.z.object({
978
+ /** Other plugins that must be installed + enabled before this one enables. */
979
+ apps: import_zod.z.array(AppDependencySchema).default([]),
980
+ /**
981
+ * Abstract capability edges the broker resolves to a bound provider at
982
+ * enable time. Mirrors `Requires::capabilities` in
983
+ * `crates/ryu-kernel-contracts` — an `apps` edge names a specific plugin; a
984
+ * `capabilities` edge names a capability and lets the binding registry choose
985
+ * the provider. Each is lowered to an app-id graph edge once bound, so the
986
+ * enable/disable/cycle machinery is shared. Empty for the common case.
987
+ */
988
+ capabilities: import_zod.z.array(CapabilityReqSchema).default([]),
989
+ /**
990
+ * Permission grants implied by the dependencies. Declaration only — the
991
+ * Gateway remains the sole authority on what a grant *allows*, and Core's
992
+ * dependency graph resolves `apps` only.
993
+ */
994
+ grants: import_zod.z.array(import_zod.z.string()).default([])
995
+ });
996
+ var SurfaceSchema = import_zod.z.enum([
997
+ /** The Ryu Gateway. */
998
+ "gateway",
999
+ /** A headless Core node (no UI). */
1000
+ "core",
1001
+ /** The Tauri desktop app. */
1002
+ "desktop",
1003
+ /** The Electron dynamic-island companion. */
1004
+ "island",
1005
+ /** The Expo/React-Native mobile app. */
1006
+ "mobile",
1007
+ /** The browser extension. */
1008
+ "extension",
1009
+ /** The Next.js web app. */
1010
+ "web",
1011
+ /** The terminal client. */
1012
+ "cli"
1013
+ ]);
1014
+ var PluginManifestSchema = import_zod.z.object({
1015
+ /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
1016
+ id: import_zod.z.string().min(1, "id is required"),
1017
+ /** Human-readable display name shown in the plugin store / launcher. */
1018
+ name: import_zod.z.string().min(1, "name is required"),
1019
+ /**
1020
+ * Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
1021
+ * whose version is not valid semver; the regex here enforces the same rule at
1022
+ * SDK-build time.
1023
+ */
1024
+ version: import_zod.z.string().regex(
1025
+ /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
1026
+ "version must be a valid semver string (e.g. 1.0.0)"
1027
+ ),
1028
+ /**
1029
+ * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
1030
+ * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
1031
+ * write it here BEFORE the manifest is signed, so the hash rides INSIDE the
1032
+ * Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
1033
+ * Core's install path recomputes the hash over the fetched code and rejects a
1034
+ * mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
1035
+ * Mirrors Core's `PluginManifest.ui_code_sha256`.
1036
+ */
1037
+ ui_code_sha256: import_zod.z.string().nullish(),
1038
+ /** The Runnables this plugin bundles. */
1039
+ runnables: import_zod.z.array(RunnableMetaSchema).default([]),
1040
+ /**
1041
+ * Permission grants this plugin declares it needs (e.g. `"mcp:web_search"`).
1042
+ * Declarations only — grant enforcement is the Gateway's responsibility.
1043
+ */
1044
+ permission_grants: import_zod.z.array(import_zod.z.string()).default([]),
1045
+ /**
1046
+ * Optional Companion surface (an in-desktop overlay or sidebar panel).
1047
+ * Absent when the plugin has no Companion surface.
1048
+ */
1049
+ companion: CompanionSurfaceSchema.optional(),
1050
+ /**
1051
+ * VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
1052
+ * `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
1053
+ * enabled flag, so `["*"]` is the usual value.
1054
+ */
1055
+ activation_events: import_zod.z.array(import_zod.z.string()).default([]),
1056
+ /**
1057
+ * Contribution points: server-side turn hooks + declarative UI widgets.
1058
+ * Absent for a plugin that contributes nothing here.
1059
+ */
1060
+ contributes: ContributesSchema.optional(),
1061
+ /**
1062
+ * **Plugin-to-plugin dependencies** — the other plugins this one needs. Core
1063
+ * resolves them into a topological enable order (dependencies enable first;
1064
+ * disabling one is refused while an enabled dependent needs it).
1065
+ *
1066
+ * Absent = **no dependencies**, the backward-compatible default. Kept
1067
+ * `.optional()` (never defaulted) so a manifest that declares none serialises
1068
+ * with no `requires` key at all, exactly like Core's
1069
+ * `#[serde(skip_serializing_if = "Option::is_none")]`.
1070
+ */
1071
+ requires: RequiresSchema.optional(),
1072
+ /**
1073
+ * Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
1074
+ * surface** — the backward-compatible default, which must never be read as
1075
+ * "runs nowhere". Core filters only when the list is explicitly non-empty, and
1076
+ * only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
1077
+ * an unsupported-target plugin stays installable and inspectable.
1078
+ */
1079
+ targets: import_zod.z.array(SurfaceSchema).default([]),
1080
+ /**
1081
+ * Optional per-item AFFILIATE terms: the commission paid to a referrer when a
1082
+ * referred user buys this (paid) item. `value` is basis points for `percent`
1083
+ * (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
1084
+ * falls back to the seller org owner's default affiliate terms. This is the
1085
+ * authoring surface for the marketplace publish body's `affiliate` field (the
1086
+ * server re-validates it); it only takes effect on a paid item.
1087
+ */
1088
+ affiliate: import_zod.z.object({
1089
+ enabled: import_zod.z.boolean().default(false),
1090
+ rule: import_zod.z.object({
1091
+ type: import_zod.z.enum(["percent", "flat"]),
1092
+ value: import_zod.z.number().nonnegative(),
1093
+ recurring: import_zod.z.boolean().default(false),
1094
+ durationMonths: import_zod.z.number().int().positive().nullish(),
1095
+ fundedBy: import_zod.z.enum(["platform", "seller"]).default("platform")
1096
+ }).optional()
1097
+ }).optional(),
1098
+ // ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
1099
+ // Optional store-listing fields a plugin author declares so the marketplace
1100
+ // detail dialog renders a richer App-Store-style preview. Field names align
1101
+ // with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
1102
+ // one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
1103
+ // rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
1104
+ // body (not inside the signed manifest blob) so the control plane stores them.
1105
+ // All optional + additive: a manifest omitting them still validates.
1106
+ /** Longer plain/markdown description shown in the detail dialog. */
1107
+ description: import_zod.z.string().optional(),
1108
+ /** Short one-line pitch shown under the name (Ryu extension). */
1109
+ tagline: import_zod.z.string().optional(),
1110
+ /**
1111
+ * Publisher identity. A bare string OR a Claude-style object; `ryu publish`
1112
+ * resolves it to the display `developer` (`author.name` when an object).
1113
+ */
1114
+ author: import_zod.z.union([
1115
+ import_zod.z.string(),
1116
+ import_zod.z.object({
1117
+ name: import_zod.z.string(),
1118
+ email: import_zod.z.string().optional(),
1119
+ url: import_zod.z.string().optional()
1120
+ })
1121
+ ]).optional(),
1122
+ /** Project/marketing homepage — maps to the listing `website` (Claude field). */
1123
+ homepage: import_zod.z.string().optional(),
1124
+ /** Free-text search keywords (Claude field). */
1125
+ keywords: import_zod.z.array(import_zod.z.string()).optional(),
1126
+ /** Taxonomy category beyond the runnable kinds (Claude field). */
1127
+ category: import_zod.z.string().optional(),
1128
+ /** SPDX-ish license identifier (Claude field). */
1129
+ license: import_zod.z.string().optional(),
1130
+ /** Square logo/icon URL for the listing card + detail header. */
1131
+ iconUrl: import_zod.z.string().optional(),
1132
+ /** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
1133
+ screenshots: import_zod.z.array(import_zod.z.string()).optional(),
1134
+ /** Privacy policy URL surfaced on detail (Ryu extension). */
1135
+ privacyPolicyUrl: import_zod.z.string().optional(),
1136
+ /** Terms-of-service URL surfaced on detail (Ryu extension). */
1137
+ termsOfServiceUrl: import_zod.z.string().optional(),
1138
+ /**
1139
+ * Human-readable capability strings (Ryu extension). When omitted the control
1140
+ * plane derives a default from `permission_grants`, so declaring this is only
1141
+ * needed to override the derived labels.
1142
+ */
1143
+ capabilities: import_zod.z.array(import_zod.z.string()).optional(),
1144
+ /** Example prompt chips shown on detail (Ryu extension). */
1145
+ examplePrompts: import_zod.z.array(import_zod.z.string()).optional(),
1146
+ /**
1147
+ * Optional companion/config card (Ryu extension): a single setup step or an
1148
+ * array of steps guiding the user through post-install configuration.
1149
+ */
1150
+ setup: import_zod.z.union([SetupStepSchema, import_zod.z.array(SetupStepSchema)]).optional()
1151
+ });
1152
+ function validatePluginId(id) {
1153
+ loadNative().validatePluginId(id);
1154
+ }
1155
+ function validateManifestStrict(manifestJson) {
1156
+ return loadNative().parseAndValidateManifest(manifestJson);
1157
+ }
1158
+ function coreManifestJsonSchema() {
1159
+ return JSON.parse(loadNative().pluginManifestJsonSchema());
1160
+ }
1161
+
1162
+ // src/runnable/app.ts
1163
+ var DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
1164
+ var DEFAULT_APP_DISPLAY_MODE = "inline";
1165
+ function appToolId(server, name) {
1166
+ return `${server}__${name}`;
1167
+ }
1168
+ function defineApp(options) {
1169
+ const server = options.server ?? options.slug;
1170
+ const uri = `ui://widget/${options.slug}.html`;
1171
+ const mime = options.mime ?? DEFAULT_APP_WIDGET_MIME;
1172
+ const displayMode = options.displayMode ?? DEFAULT_APP_DISPLAY_MODE;
1173
+ const hasCompanions = options.tools.some((t) => t.accessible === true);
1174
+ const runnables = [];
1175
+ const widgets = [];
1176
+ for (const spec of options.tools) {
1177
+ const isRender = spec.accessible !== true;
1178
+ const id = appToolId(server, spec.name);
1179
+ const config = {
1180
+ slug: id,
1181
+ description: spec.description,
1182
+ widget: isRender,
1183
+ widget_accessible: isRender ? hasCompanions : true,
1184
+ ...spec.inputSchema ? { input_schema: spec.inputSchema } : {},
1185
+ ...spec.invoking ? { invoking: spec.invoking } : {},
1186
+ ...spec.invoked ? { invoked: spec.invoked } : {}
1187
+ };
1188
+ runnables.push({
1189
+ id,
1190
+ name: spec.name,
1191
+ kind: "tool",
1192
+ config
1193
+ });
1194
+ if (isRender) {
1195
+ widgets.push({
1196
+ tool_id: id,
1197
+ uri,
1198
+ ui_entry: options.uiEntry,
1199
+ mime,
1200
+ default_display_mode: displayMode
1201
+ });
1202
+ }
1203
+ }
1204
+ const contributes = {
1205
+ turn_hooks: [],
1206
+ composer_controls: [],
1207
+ settings_tabs: [],
1208
+ slash_commands: [],
1209
+ widgets
1210
+ };
1211
+ const raw = {
1212
+ id: options.id,
1213
+ name: options.title,
1214
+ version: options.version,
1215
+ runnables,
1216
+ permission_grants: options.grants ?? [],
1217
+ activation_events: options.activationEvents ?? ["*"],
1218
+ contributes,
1219
+ // `targets: []` means EVERY surface, so an app that declares none is
1220
+ // unrestricted — the backward-compatible default.
1221
+ targets: options.targets ?? [],
1222
+ // `requires` stays ABSENT (not `{apps:[],grants:[]}`) when undeclared, so the
1223
+ // emitted manifest carries no key at all — matching Core's
1224
+ // `Option<Requires>` + `skip_serializing_if = "Option::is_none"`.
1225
+ ...options.requires ? {
1226
+ requires: {
1227
+ apps: options.requires.apps ?? [],
1228
+ capabilities: options.requires.capabilities ?? [],
1229
+ grants: options.requires.grants ?? []
1230
+ }
1231
+ } : {}
1232
+ };
1233
+ const result = PluginManifestSchema.safeParse(raw);
1234
+ if (!result.success) {
1235
+ const first = result.error.issues[0];
1236
+ const field = first?.path.join(".") ?? "unknown";
1237
+ const message = first?.message ?? "validation failed";
1238
+ throw new Error(`plugin.json validation failed at '${field}': ${message}`);
1239
+ }
1240
+ return result.data;
1241
+ }
1242
+
1243
+ // src/builder.ts
1244
+ var RunnableBuilder = class {
1245
+ _id = "";
1246
+ _name = "";
1247
+ id(value) {
1248
+ this._id = value;
1249
+ return this;
1250
+ }
1251
+ name(value) {
1252
+ this._name = value;
1253
+ return this;
1254
+ }
1255
+ };
1256
+ var AgentBuilder = class extends RunnableBuilder {
1257
+ build() {
1258
+ const result = RunnableMetaSchema.safeParse({
1259
+ id: this._id,
1260
+ name: this._name,
1261
+ kind: "agent"
1262
+ });
1263
+ if (!result.success) {
1264
+ throw new Error(
1265
+ `Invalid agent runnable: ${result.error.issues.map((i) => i.message).join("; ")}`
1266
+ );
1267
+ }
1268
+ return result.data;
1269
+ }
1270
+ };
1271
+ var WorkflowBuilder = class extends RunnableBuilder {
1272
+ build() {
1273
+ const result = RunnableMetaSchema.safeParse({
1274
+ id: this._id,
1275
+ name: this._name,
1276
+ kind: "workflow"
1277
+ });
1278
+ if (!result.success) {
1279
+ throw new Error(
1280
+ `Invalid workflow runnable: ${result.error.issues.map((i) => i.message).join("; ")}`
1281
+ );
1282
+ }
1283
+ return result.data;
1284
+ }
1285
+ };
1286
+ var ToolBuilder = class extends RunnableBuilder {
1287
+ build() {
1288
+ const result = RunnableMetaSchema.safeParse({
1289
+ id: this._id,
1290
+ name: this._name,
1291
+ kind: "tool"
1292
+ });
1293
+ if (!result.success) {
1294
+ throw new Error(
1295
+ `Invalid tool runnable: ${result.error.issues.map((i) => i.message).join("; ")}`
1296
+ );
1297
+ }
1298
+ return result.data;
1299
+ }
1300
+ };
1301
+ var SkillBuilder = class extends RunnableBuilder {
1302
+ build() {
1303
+ const result = RunnableMetaSchema.safeParse({
1304
+ id: this._id,
1305
+ name: this._name,
1306
+ kind: "skill"
1307
+ });
1308
+ if (!result.success) {
1309
+ throw new Error(
1310
+ `Invalid skill runnable: ${result.error.issues.map((i) => i.message).join("; ")}`
1311
+ );
1312
+ }
1313
+ return result.data;
1314
+ }
1315
+ };
1316
+ var agent = () => new AgentBuilder();
1317
+ var workflow = () => new WorkflowBuilder();
1318
+ var tool = () => new ToolBuilder();
1319
+ var skill = () => new SkillBuilder();
1320
+ var PluginBuilder = class {
1321
+ _id = "";
1322
+ _name = "";
1323
+ _version = "";
1324
+ _runnables = [];
1325
+ _grants = [];
1326
+ _companion = void 0;
1327
+ _dependencies = [];
1328
+ _requiredCapabilities = [];
1329
+ _requiredGrants = [];
1330
+ _targets = [];
1331
+ /** Set the reverse-domain app id (e.g. `"com.example.my-app"`). */
1332
+ id(value) {
1333
+ this._id = value;
1334
+ return this;
1335
+ }
1336
+ /** Set the human-readable display name. */
1337
+ name(value) {
1338
+ this._name = value;
1339
+ return this;
1340
+ }
1341
+ /** Set the semver version string (e.g. `"1.0.0"`). */
1342
+ version(value) {
1343
+ this._version = value;
1344
+ return this;
1345
+ }
1346
+ /** Append a pre-built `RunnableMeta` (from any per-kind builder). */
1347
+ runnable(meta) {
1348
+ this._runnables.push(meta);
1349
+ return this;
1350
+ }
1351
+ /** Declare a permission grant (e.g. `"mcp:web_search"`). */
1352
+ grant(permission) {
1353
+ this._grants.push(permission);
1354
+ return this;
1355
+ }
1356
+ /** Set an optional Companion surface descriptor. */
1357
+ companion(surface) {
1358
+ this._companion = surface;
1359
+ return this;
1360
+ }
1361
+ /**
1362
+ * Declare a **plugin-to-plugin dependency**: `id` must be installed and is
1363
+ * auto-enabled (in dependency order) before this plugin enables.
1364
+ *
1365
+ * `minVersion` is a MINIMUM — a bare `"1.2.0"` means `">=1.2.0"`, so an
1366
+ * installed `2.0.0` satisfies it (comparator syntax like `">=1.2, <2"` is
1367
+ * honoured verbatim).
1368
+ */
1369
+ dependsOn(id, minVersion) {
1370
+ this._dependencies.push(
1371
+ minVersion ? { id, min_version: minVersion } : { id }
1372
+ );
1373
+ return this;
1374
+ }
1375
+ /**
1376
+ * Declare a permission grant implied by this plugin's dependencies
1377
+ * (`requires.grants`). Declaration only — the Gateway remains the sole
1378
+ * authority on what a grant allows. Use {@link PluginBuilder.grant} for the
1379
+ * grants this plugin needs in its own right.
1380
+ */
1381
+ requiredGrant(permission) {
1382
+ this._requiredGrants.push(permission);
1383
+ return this;
1384
+ }
1385
+ /**
1386
+ * Declare an abstract **capability** edge (`requires.capabilities`) the broker
1387
+ * resolves to a bound provider at enable time — e.g. `requiresCapability("rag")`.
1388
+ * Distinct from a specific-plugin dependency: a capability edge lets the
1389
+ * binding registry choose the provider. `minVersion` is a MINIMUM (`"1.2.0"`
1390
+ * = `">=1.2.0"`).
1391
+ */
1392
+ requiresCapability(capability, minVersion) {
1393
+ this._requiredCapabilities.push(
1394
+ minVersion ? { capability, min_version: minVersion } : { capability }
1395
+ );
1396
+ return this;
1397
+ }
1398
+ /**
1399
+ * Restrict this plugin to a host surface (`"desktop"`, `"island"`, …).
1400
+ * Declaring NO target is the default and means **every** surface.
1401
+ */
1402
+ target(surface) {
1403
+ this._targets.push(surface);
1404
+ return this;
1405
+ }
1406
+ /**
1407
+ * Validate and return the assembled `PluginManifest`. Throws an `Error` with
1408
+ * the failing field name and message when validation fails.
1409
+ */
1410
+ build() {
1411
+ const hasRequires = this._dependencies.length > 0 || this._requiredCapabilities.length > 0 || this._requiredGrants.length > 0;
1412
+ const raw = {
1413
+ id: this._id,
1414
+ name: this._name,
1415
+ version: this._version,
1416
+ runnables: this._runnables,
1417
+ permission_grants: this._grants,
1418
+ companion: this._companion,
1419
+ targets: this._targets,
1420
+ ...hasRequires ? {
1421
+ requires: {
1422
+ apps: this._dependencies,
1423
+ capabilities: this._requiredCapabilities,
1424
+ grants: this._requiredGrants
1425
+ }
1426
+ } : {}
1427
+ };
1428
+ const result = PluginManifestSchema.safeParse(raw);
1429
+ if (!result.success) {
1430
+ const first = result.error.issues[0];
1431
+ const field = first?.path.join(".") ?? "unknown";
1432
+ const message = first?.message ?? "validation failed";
1433
+ throw new Error(
1434
+ `plugin.json validation failed at '${field}': ${message}`
1435
+ );
1436
+ }
1437
+ return result.data;
1438
+ }
1439
+ };
1440
+ var AppBuilder = class {
1441
+ _id = "";
1442
+ _title = "";
1443
+ _version = "";
1444
+ _slug = "";
1445
+ _server = void 0;
1446
+ _displayMode = void 0;
1447
+ _mime = void 0;
1448
+ _uiEntry = "";
1449
+ _grants = [];
1450
+ _activationEvents = [];
1451
+ _tools = [];
1452
+ _dependencies = [];
1453
+ _requiredCapabilities = [];
1454
+ _requiredGrants = [];
1455
+ _targets = [];
1456
+ /** Set the reverse-domain app id (e.g. `"com.example.checklist"`). */
1457
+ id(value) {
1458
+ this._id = value;
1459
+ return this;
1460
+ }
1461
+ /** Set the human-readable display name. */
1462
+ title(value) {
1463
+ this._title = value;
1464
+ return this;
1465
+ }
1466
+ /** Set the semver version string (e.g. `"1.0.0"`). */
1467
+ version(value) {
1468
+ this._version = value;
1469
+ return this;
1470
+ }
1471
+ /** Set the app slug (drives `ui://widget/<slug>.html` and the server default). */
1472
+ slug(value) {
1473
+ this._slug = value;
1474
+ return this;
1475
+ }
1476
+ /** Override the MCP server namespace for tool ids (defaults to the slug). */
1477
+ server(value) {
1478
+ this._server = value;
1479
+ return this;
1480
+ }
1481
+ /** Set the default widget display mode (`inline` | `fullscreen` | `pip`). */
1482
+ displayMode(value) {
1483
+ this._displayMode = value;
1484
+ return this;
1485
+ }
1486
+ /** Override the widget MIME dialect (defaults to `text/html+skybridge`). */
1487
+ mime(value) {
1488
+ this._mime = value;
1489
+ return this;
1490
+ }
1491
+ /** Set the widget UI source entry `ryu pack` bundles into `ui_code`. */
1492
+ uiEntry(value) {
1493
+ this._uiEntry = value;
1494
+ return this;
1495
+ }
1496
+ /** Declare a permission grant (e.g. `"mcp:web_search"`). */
1497
+ grant(permission) {
1498
+ this._grants.push(permission);
1499
+ return this;
1500
+ }
1501
+ /** Add a VS-Code-style activation event (empty = eager `["*"]`). */
1502
+ activationEvent(event) {
1503
+ this._activationEvents.push(event);
1504
+ return this;
1505
+ }
1506
+ /** Append a tool spec (render tool unless `accessible:true`). */
1507
+ tool(spec) {
1508
+ this._tools.push(spec);
1509
+ return this;
1510
+ }
1511
+ /**
1512
+ * Declare a **plugin-to-plugin dependency** (auto-enabled, in dependency order,
1513
+ * before this app). `minVersion` is a MINIMUM (`"1.2.0"` = `">=1.2.0"`).
1514
+ */
1515
+ dependsOn(id, minVersion) {
1516
+ this._dependencies.push(
1517
+ minVersion ? { id, min_version: minVersion } : { id }
1518
+ );
1519
+ return this;
1520
+ }
1521
+ /** Declare a grant implied by this app's dependencies (`requires.grants`). */
1522
+ requiredGrant(permission) {
1523
+ this._requiredGrants.push(permission);
1524
+ return this;
1525
+ }
1526
+ /**
1527
+ * Declare an abstract **capability** edge (`requires.capabilities`) the broker
1528
+ * resolves to a bound provider at enable time — e.g. `requiresCapability("rag")`.
1529
+ * Distinct from a specific-plugin dependency: a capability edge lets the
1530
+ * binding registry choose the provider. `minVersion` is a MINIMUM (`"1.2.0"`
1531
+ * = `">=1.2.0"`).
1532
+ */
1533
+ requiresCapability(capability, minVersion) {
1534
+ this._requiredCapabilities.push(
1535
+ minVersion ? { capability, min_version: minVersion } : { capability }
1536
+ );
1537
+ return this;
1538
+ }
1539
+ /** Restrict this app to a host surface. No target = every surface. */
1540
+ target(surface) {
1541
+ this._targets.push(surface);
1542
+ return this;
1543
+ }
1544
+ /**
1545
+ * Validate and return the assembled `PluginManifest`. Throws an `Error` naming
1546
+ * the failing field when validation fails.
1547
+ */
1548
+ build() {
1549
+ const hasRequires = this._dependencies.length > 0 || this._requiredCapabilities.length > 0 || this._requiredGrants.length > 0;
1550
+ const options = {
1551
+ id: this._id,
1552
+ title: this._title,
1553
+ version: this._version,
1554
+ slug: this._slug,
1555
+ uiEntry: this._uiEntry,
1556
+ tools: this._tools,
1557
+ grants: this._grants,
1558
+ ...this._server ? { server: this._server } : {},
1559
+ ...this._displayMode ? { displayMode: this._displayMode } : {},
1560
+ ...this._mime ? { mime: this._mime } : {},
1561
+ ...this._activationEvents.length > 0 ? { activationEvents: this._activationEvents } : {},
1562
+ ...hasRequires ? {
1563
+ requires: {
1564
+ apps: this._dependencies,
1565
+ capabilities: this._requiredCapabilities,
1566
+ grants: this._requiredGrants
1567
+ }
1568
+ } : {},
1569
+ ...this._targets.length > 0 ? { targets: this._targets } : {}
1570
+ };
1571
+ return defineApp(options);
1572
+ }
1573
+ };
1574
+ var app = () => new AppBuilder();
1575
+
1576
+ // src/runnable/agent.ts
1577
+ var CAPABILITY_SLOTS = ["rag", "memory", "tts", "stt"];
1578
+ function lowerChatSlot(chat) {
1579
+ if (chat === void 0) {
1580
+ return {};
1581
+ }
1582
+ if (typeof chat === "string") {
1583
+ return { model: chat };
1584
+ }
1585
+ return {
1586
+ model: chat.model,
1587
+ engine: chat.engine,
1588
+ persona: chat.persona,
1589
+ modelPrefKey: chat.modelPrefKey
1590
+ };
1591
+ }
1592
+ function lowerCapabilitySlot(capability, slot, card) {
1593
+ if (slot === void 0 || slot === false) {
1594
+ return;
1595
+ }
1596
+ if (slot === true) {
1597
+ card.capabilities.push({ capability });
1598
+ return;
1599
+ }
1600
+ if (typeof slot === "string") {
1601
+ card.capabilities.push({ capability });
1602
+ card.providers[capability] = slot;
1603
+ return;
1604
+ }
1605
+ card.capabilities.push(
1606
+ slot.minVersion ? { capability, min_version: slot.minVersion } : { capability }
1607
+ );
1608
+ if (slot.provider) {
1609
+ card.providers[capability] = slot.provider;
1610
+ }
1611
+ }
1612
+ function lowerSlots(options) {
1613
+ const card = {
1614
+ ...lowerChatSlot(options.chat),
1615
+ capabilities: [],
1616
+ providers: {},
1617
+ tools: []
1618
+ };
1619
+ for (const capability of CAPABILITY_SLOTS) {
1620
+ lowerCapabilitySlot(capability, options[capability], card);
1621
+ }
1622
+ for (const t of options.tools ?? []) {
1623
+ card.tools.push(typeof t === "string" ? t : t.id);
1624
+ }
1625
+ return card;
1626
+ }
1627
+ function defaultRun(card) {
1628
+ return async (input, ctx) => {
1629
+ const content = typeof input === "string" ? input : JSON.stringify(input ?? "");
1630
+ const messages = card.persona ? [
1631
+ { role: "system", content: card.persona },
1632
+ { role: "user", content }
1633
+ ] : [{ role: "user", content }];
1634
+ const result = await ctx.gateway.chat(messages);
1635
+ return result.content;
1636
+ };
1637
+ }
1638
+ function cardConfig(card) {
1639
+ const config = {};
1640
+ if (card.model) {
1641
+ config.model = card.model;
1642
+ }
1643
+ if (card.engine) {
1644
+ config.engine = card.engine;
1645
+ }
1646
+ if (card.persona) {
1647
+ config.persona = card.persona;
1648
+ }
1649
+ if (card.modelPrefKey) {
1650
+ config.model_pref_key = card.modelPrefKey;
1651
+ }
1652
+ if (Object.keys(card.providers).length > 0) {
1653
+ config.capability_providers = card.providers;
1654
+ }
1655
+ if (card.tools.length > 0) {
1656
+ config.tools = card.tools;
1657
+ }
1658
+ return config;
1659
+ }
1660
+ function agentToManifest(agent2, options) {
1661
+ const config = cardConfig(agent2.card);
1662
+ const meta = {
1663
+ id: agent2.id,
1664
+ name: agent2.name,
1665
+ kind: "agent",
1666
+ ...Object.keys(config).length > 0 ? { config } : {}
1667
+ };
1668
+ const hasRequires = agent2.card.capabilities.length > 0 || (options.grants?.length ?? 0) > 0;
1669
+ const raw = {
1670
+ id: options.id,
1671
+ name: options.name ?? agent2.name,
1672
+ version: options.version,
1673
+ runnables: [meta],
1674
+ ...hasRequires ? {
1675
+ requires: {
1676
+ apps: [],
1677
+ capabilities: agent2.card.capabilities,
1678
+ grants: options.grants ?? []
1679
+ }
1680
+ } : {}
1681
+ };
1682
+ const result = PluginManifestSchema.safeParse(raw);
1683
+ if (!result.success) {
1684
+ const first = result.error.issues[0];
1685
+ const field = first?.path.join(".") ?? "unknown";
1686
+ const message = first?.message ?? "validation failed";
1687
+ throw new Error(
1688
+ `agent manifest validation failed at '${field}': ${message}`
1689
+ );
1690
+ }
1691
+ return result.data;
1692
+ }
1693
+ function defineAgent(options) {
1694
+ const { id, name } = options;
1695
+ const card = lowerSlots(options);
1696
+ const run = options.run ?? defaultRun(card);
1697
+ return {
1698
+ id,
1699
+ name,
1700
+ kind: "agent",
1701
+ run,
1702
+ card,
1703
+ toManifest(manifestOptions) {
1704
+ return agentToManifest(this, manifestOptions);
1705
+ }
1706
+ };
1707
+ }
1708
+
1709
+ // src/runnable/skill.ts
1710
+ function defineSkill(options) {
1711
+ const { id, name, run } = options;
1712
+ return {
1713
+ id,
1714
+ name,
1715
+ kind: "skill",
1716
+ run
1717
+ };
1718
+ }
1719
+
1720
+ // src/runnable/tool.ts
1721
+ function validateInput(input, schema, toolId) {
1722
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
1723
+ throw new Error(
1724
+ `[ryu-sdk] Tool "${toolId}" input must be an object, got ${JSON.stringify(input)}`
1725
+ );
1726
+ }
1727
+ const record = input;
1728
+ for (const requiredKey of schema.required ?? []) {
1729
+ if (!(requiredKey in record)) {
1730
+ throw new Error(
1731
+ `[ryu-sdk] Tool "${toolId}" input missing required field "${requiredKey}"`
1732
+ );
1733
+ }
1734
+ }
1735
+ for (const [key, prop] of Object.entries(schema.properties)) {
1736
+ if (!(key in record)) {
1737
+ continue;
1738
+ }
1739
+ const value = record[key];
1740
+ if (!checkType(value, prop.type)) {
1741
+ throw new Error(
1742
+ `[ryu-sdk] Tool "${toolId}" input field "${key}" expected type "${prop.type}", got ${JSON.stringify(value)}`
1743
+ );
1744
+ }
1745
+ }
1746
+ }
1747
+ function checkType(value, type) {
1748
+ switch (type) {
1749
+ case "string":
1750
+ return typeof value === "string";
1751
+ case "number":
1752
+ case "integer":
1753
+ return typeof value === "number";
1754
+ case "boolean":
1755
+ return typeof value === "boolean";
1756
+ case "array":
1757
+ return Array.isArray(value);
1758
+ case "object":
1759
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1760
+ default:
1761
+ return false;
1762
+ }
1763
+ }
1764
+ function defineTool(options) {
1765
+ const { id, name, schema, run } = options;
1766
+ const code = `return await (${run.toString()})(input, host);`;
1767
+ return {
1768
+ id,
1769
+ name,
1770
+ kind: "tool",
1771
+ schema,
1772
+ code,
1773
+ run(input, ctx) {
1774
+ validateInput(input, schema, id);
1775
+ return run(input, ctx);
1776
+ }
1777
+ };
1778
+ }
1779
+ function inlineToolRunnable(tool2, options) {
1780
+ return {
1781
+ id: tool2.id,
1782
+ name: tool2.name,
1783
+ kind: "tool",
1784
+ config: {
1785
+ slug: tool2.id,
1786
+ backend: "inline_deno",
1787
+ code: tool2.code,
1788
+ input_schema: tool2.schema,
1789
+ ...options?.description ? { description: options.description } : {}
1790
+ }
1791
+ };
1792
+ }
1793
+
1794
+ // src/runnable/turn-hook.ts
1795
+ function defineTurnHook(options) {
1796
+ const source = options.run.toString();
1797
+ const code = `return await (${source})(ctx, host);`;
1798
+ return {
1799
+ id: options.id,
1800
+ on: options.on ?? "post_assistant_turn",
1801
+ code
1802
+ };
1803
+ }
1804
+ function definePlugin(options) {
1805
+ const contributes = {
1806
+ turn_hooks: options.turnHooks ?? [],
1807
+ composer_controls: options.composerControls ?? [],
1808
+ settings_tabs: options.settingsTabs ?? [],
1809
+ slash_commands: options.slashCommands ?? [],
1810
+ // A turn-hook plugin contributes no app widgets; the field is required on the
1811
+ // resolved `Contributes` type (zod default applied), so set it explicitly.
1812
+ widgets: []
1813
+ };
1814
+ const tools = options.tools ?? [];
1815
+ const runnables = tools.map((t) => inlineToolRunnable(t));
1816
+ const grants = new Set(options.grants ?? []);
1817
+ if (tools.length > 0) {
1818
+ grants.add("tool:execute");
1819
+ }
1820
+ return {
1821
+ id: options.id,
1822
+ name: options.name,
1823
+ version: options.version,
1824
+ runnables,
1825
+ permission_grants: [...grants],
1826
+ activation_events: options.activationEvents ?? ["*"],
1827
+ contributes,
1828
+ // Empty = EVERY surface (Core's backward-compatible default), never "hidden".
1829
+ targets: options.targets ?? [],
1830
+ // Absent (not `{apps:[],grants:[]}`) when undeclared, matching Core's
1831
+ // `Option<Requires>` + `skip_serializing_if = "Option::is_none"`.
1832
+ ...options.requires ? {
1833
+ requires: {
1834
+ apps: options.requires.apps ?? [],
1835
+ capabilities: options.requires.capabilities ?? [],
1836
+ grants: options.requires.grants ?? []
1837
+ }
1838
+ } : {}
1839
+ };
1840
+ }
1841
+
1842
+ // src/runnable/workflow.ts
1843
+ function defineWorkflow(options) {
1844
+ const { id, name, run } = options;
1845
+ return {
1846
+ id,
1847
+ name,
1848
+ kind: "workflow",
1849
+ run
1850
+ };
1851
+ }
1852
+ // Annotate the CommonJS export names for ESM import in node:
1853
+ 0 && (module.exports = {
1854
+ Agent,
1855
+ AgentBuilder,
1856
+ AppBuilder,
1857
+ AppDependencySchema,
1858
+ CapabilityReqSchema,
1859
+ CompanionSurfaceSchema,
1860
+ DEFAULT_GATEWAY_URL,
1861
+ ModelClient,
1862
+ PRIMITIVE_BINDINGS,
1863
+ PluginBuilder,
1864
+ PluginManifestSchema,
1865
+ RequiresSchema,
1866
+ RunnableKindSchema,
1867
+ RunnableMetaSchema,
1868
+ SkillBuilder,
1869
+ SurfaceSchema,
1870
+ ToolAppConfigSchema,
1871
+ ToolBuilder,
1872
+ WidgetContributionSchema,
1873
+ WorkflowBuilder,
1874
+ agent,
1875
+ app,
1876
+ appToolId,
1877
+ assertAllowedEgressUrl,
1878
+ coreManifestJsonSchema,
1879
+ createAgent,
1880
+ createPrimitives,
1881
+ defineAgent,
1882
+ defineApp,
1883
+ defineModel,
1884
+ definePlugin,
1885
+ defineSkill,
1886
+ defineTool,
1887
+ defineTurnHook,
1888
+ defineWorkflow,
1889
+ httpPrimitiveTransport,
1890
+ inlineToolRunnable,
1891
+ query,
1892
+ resolveGatewayToken,
1893
+ resolveGatewayUrl,
1894
+ ryuTool,
1895
+ skill,
1896
+ tool,
1897
+ validateManifestStrict,
1898
+ validatePluginId,
1899
+ workflow
1900
+ });