@ryuhq/sdk 0.1.14 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +69 -7
  2. package/dist/action.cjs +839 -0
  3. package/dist/action.d.cts +88 -0
  4. package/dist/action.d.ts +88 -0
  5. package/dist/action.js +8 -0
  6. package/dist/agent-plugin.cjs +46 -0
  7. package/dist/agent-plugin.d.cts +43 -33
  8. package/dist/agent-plugin.d.ts +43 -33
  9. package/dist/agent-plugin.js +1 -1
  10. package/dist/agent.cjs +16 -0
  11. package/dist/agent.d.cts +377 -1
  12. package/dist/agent.d.ts +377 -1
  13. package/dist/agent.js +4 -2
  14. package/dist/app-B0Z9Ew_R.d.cts +141 -0
  15. package/dist/app-C-BDJwfG.d.ts +141 -0
  16. package/dist/builder.cjs +1194 -0
  17. package/dist/builder.d.cts +217 -0
  18. package/dist/builder.d.ts +217 -0
  19. package/dist/builder.js +29 -0
  20. package/dist/chunk-4TPUZDTI.js +94 -0
  21. package/dist/chunk-BC3A7HMO.js +164 -0
  22. package/dist/chunk-FZSFZOIN.js +200 -0
  23. package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
  24. package/dist/chunk-HLKJZAFK.js +361 -0
  25. package/dist/chunk-IKEDLLFY.js +19 -0
  26. package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
  27. package/dist/{chunk-AO2KJRDD.js → chunk-NZKVOSC2.js} +192 -3
  28. package/dist/chunk-QYFUNJOH.js +83 -0
  29. package/dist/chunk-SN2QBJUF.js +148 -0
  30. package/dist/chunk-TLDPEGC7.js +21 -0
  31. package/dist/chunk-TXSHHZF2.js +0 -0
  32. package/dist/chunk-VLIRNNAE.js +154 -0
  33. package/dist/{chunk-MTUBUPIV.js → chunk-Z57QDDJR.js} +8 -227
  34. package/dist/chunk-ZTJWBRUL.js +282 -0
  35. package/dist/cli.cjs +347 -18
  36. package/dist/cli.js +128 -23
  37. package/dist/client-D5U6ssPc.d.cts +84 -0
  38. package/dist/client-D5U6ssPc.d.ts +84 -0
  39. package/dist/index.cjs +810 -86
  40. package/dist/index.d.cts +19 -634
  41. package/dist/index.d.ts +19 -634
  42. package/dist/index.js +82 -699
  43. package/dist/manifest.cjs +200 -4
  44. package/dist/manifest.d.cts +260 -5
  45. package/dist/manifest.d.ts +260 -5
  46. package/dist/manifest.js +15 -1
  47. package/dist/mcp/client.cjs +180 -0
  48. package/dist/mcp/client.d.cts +49 -0
  49. package/dist/mcp/client.d.ts +49 -0
  50. package/dist/mcp/client.js +10 -0
  51. package/dist/mcp/server.cjs +370 -0
  52. package/dist/mcp/server.d.cts +127 -0
  53. package/dist/mcp/server.d.ts +127 -0
  54. package/dist/mcp/server.js +9 -0
  55. package/dist/mcp.cjs +376 -0
  56. package/dist/mcp.d.cts +2 -0
  57. package/dist/mcp.d.ts +2 -0
  58. package/dist/mcp.js +17 -0
  59. package/dist/model.cjs +141 -0
  60. package/dist/model.d.cts +33 -0
  61. package/dist/model.d.ts +33 -0
  62. package/dist/model.js +18 -0
  63. package/dist/plugin.cjs +46 -0
  64. package/dist/plugin.d.cts +215 -0
  65. package/dist/plugin.d.ts +215 -0
  66. package/dist/plugin.js +8 -0
  67. package/dist/runnable.cjs +1438 -0
  68. package/dist/runnable.d.cts +274 -0
  69. package/dist/runnable.d.ts +274 -0
  70. package/dist/runnable.js +34 -0
  71. package/dist/{index-B6SkaAjJ.d.ts → tool-AjkdFvhE.d.ts} +88 -461
  72. package/dist/{index-BvAB5eMk.d.cts → tool-CgzW92O_.d.cts} +88 -461
  73. package/package.json +48 -3
  74. package/src/agent/loop.test.ts +4 -4
  75. package/src/agent/tools.ts +3 -3
  76. package/src/agent-plugin.test.ts +58 -0
  77. package/src/agent-plugin.ts +109 -36
  78. package/src/builder.ts +9 -0
  79. package/src/cli/dev.test.ts +26 -47
  80. package/src/cli/dev.ts +10 -2
  81. package/src/cli-security.test.ts +109 -0
  82. package/src/cli.ts +141 -25
  83. package/src/contracts-lockstep.test.ts +16 -2
  84. package/src/exports-lockstep.test.ts +93 -0
  85. package/src/generated/plugin-manifest.ts +322 -29
  86. package/src/index.ts +53 -0
  87. package/src/manifest-schema.test.ts +30 -1
  88. package/src/manifest.fixtures.test.ts +22 -6
  89. package/src/manifest.test.ts +165 -10
  90. package/src/manifest.ts +454 -188
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/server.ts +2 -1
  93. package/src/model/index.ts +22 -0
  94. package/src/plugin/ryu-plugin.ts +82 -0
  95. package/src/runnable/action.test.ts +128 -0
  96. package/src/runnable/action.ts +202 -0
  97. package/src/runnable/app.test.ts +2 -0
  98. package/src/runnable/app.ts +50 -14
  99. package/src/runnable/index.ts +20 -3
  100. package/src/runnable/primitives.test.ts +34 -0
  101. package/src/runnable/primitives.ts +116 -0
  102. package/src/runnable/runnable-types.ts +3 -0
  103. package/src/runnable/tool.ts +36 -5
  104. package/src/runnable/turn-hook.ts +8 -3
  105. package/src/slash-command.test.ts +69 -0
@@ -0,0 +1,1438 @@
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/runnable/index.ts
31
+ var runnable_exports = {};
32
+ __export(runnable_exports, {
33
+ PRIMITIVE_BINDINGS: () => PRIMITIVE_BINDINGS,
34
+ appToolId: () => appToolId,
35
+ createPrimitives: () => createPrimitives,
36
+ defineAction: () => defineAction,
37
+ defineAgent: () => defineAgent,
38
+ defineApp: () => defineApp,
39
+ defineSkill: () => defineSkill,
40
+ defineTool: () => defineTool,
41
+ defineWorkflow: () => defineWorkflow,
42
+ httpPrimitiveTransport: () => httpPrimitiveTransport
43
+ });
44
+ module.exports = __toCommonJS(runnable_exports);
45
+
46
+ // src/manifest.ts
47
+ var import_node_module = require("module");
48
+ var import_zod = require("zod");
49
+ var RunnableKindSchema = import_zod.z.enum([
50
+ "agent",
51
+ "workflow",
52
+ "tool",
53
+ "skill",
54
+ // A companion surface (in-desktop panel). Added so a packable plugin can
55
+ // declare a companion runnable that flows through Core's existing Companion
56
+ // handler → app_contrib → `GET /api/plugins/contributions` → the desktop
57
+ // `/plugin/<id>` route. Its `config.ui_entry` (see `CompanionRunnableConfigSchema`)
58
+ // is what `ryu pack` bundles into `ui_code`.
59
+ "companion"
60
+ ]);
61
+ var RunnableMetaSchema = import_zod.z.object({
62
+ /** Stable unique identifier (e.g. `"agent-researcher"`). */
63
+ id: import_zod.z.string().min(1),
64
+ /** Human-readable display name. */
65
+ name: import_zod.z.string().min(1),
66
+ /** Which kind of runnable this entry describes. */
67
+ kind: RunnableKindSchema,
68
+ /**
69
+ * Optional per-kind config blob. Mirrors Core's `RunnableEntry.config`
70
+ * (`Option<serde_json::Value>`) so a manifest authored here round-trips
71
+ * through Core-strict validation. Left opaque (a record) at this authoring
72
+ * layer; the per-kind shape is enforced by Core's `validate_runnable`.
73
+ *
74
+ * For a `companion` runnable, `config.ui_entry` names the plugin's UI entry
75
+ * module (relative to the manifest dir). `ryu pack` bundles that entry into
76
+ * the emitted `ui_code`; Core's `CompanionConfig.ui_entry` is the lockstep
77
+ * field so a packed companion validates.
78
+ */
79
+ config: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
80
+ });
81
+ function labelImpersonatesSystemChrome(label) {
82
+ const lower = label.toLowerCase();
83
+ return lower.includes("ryu") || lower.includes("system");
84
+ }
85
+ var CompanionSurfaceSchema = import_zod.z.object({
86
+ /** Display label for the companion panel tab or tooltip. Anti-impersonation:
87
+ * may not pose as first-party Ryu/system chrome (see
88
+ * {@link labelImpersonatesSystemChrome}). */
89
+ label: import_zod.z.string().min(1).refine((value) => !labelImpersonatesSystemChrome(value), {
90
+ message: "companion label must not impersonate system chrome (must not contain 'ryu' or 'system')"
91
+ }),
92
+ /** Icon identifier resolved by the desktop shell. */
93
+ icon: import_zod.z.string().optional(),
94
+ /** Keyboard shortcut string (e.g. `"ctrl+shift+r"`). */
95
+ shortcut: import_zod.z.string().optional()
96
+ });
97
+ var TurnHookContributionSchema = import_zod.z.object({
98
+ /** Stable id for this hook (unique within the plugin). */
99
+ id: import_zod.z.string().min(1),
100
+ /** Turn boundary this fires on. Today only `"post_assistant_turn"`. */
101
+ on: import_zod.z.string().min(1).default("post_assistant_turn"),
102
+ /** The JS hook body executed in the sandbox (returns a directive). */
103
+ code: import_zod.z.string().min(1).optional(),
104
+ /** Path to the hook body, relative to the plugin root (`hooks/<name>.js`). */
105
+ code_file: import_zod.z.string().min(1).optional(),
106
+ /**
107
+ * Cheap pre-gate mirroring Core's `HookMatch` (serde name `match` on
108
+ * `TurnHookContribution.run_when`). MUST round-trip through this schema:
109
+ * `ryu pack`/`publish` persist `safeParse(...).data`, so a field missing here
110
+ * is silently STRIPPED before signing — a tool-gated `pre_tool_use` hook
111
+ * (e.g. `tools: ["bash*"]`) would lose its gate and run on EVERY tool call.
112
+ */
113
+ match: import_zod.z.object({
114
+ /** Run only if the request set this composer flag true. */
115
+ flag: import_zod.z.string().optional(),
116
+ /** Run if the last user message starts with any of these prefixes. */
117
+ commands: import_zod.z.array(import_zod.z.string()).default([]),
118
+ /** Run if the plugin has stored state for this conversation. */
119
+ stateful: import_zod.z.boolean().default(false),
120
+ /** Run if `ctx.tool_name` matches any of these `*`-wildcard patterns. */
121
+ tools: import_zod.z.array(import_zod.z.string()).default([])
122
+ }).optional()
123
+ }).refine((h) => Boolean(h.code) !== Boolean(h.code_file), {
124
+ message: "a turn hook must declare exactly one of 'code' (inline body) or 'code_file' (path to hooks/<name>.js)",
125
+ path: ["code_file"]
126
+ });
127
+ var HookEventContributionSchema = import_zod.z.object({
128
+ /** Fully-qualified event id: `<plugin id>#<event name>`, e.g. `@acme/meetings#meeting.ended`. */
129
+ id: import_zod.z.string().min(1),
130
+ /** Human-readable title for the event picker. */
131
+ title: import_zod.z.string().min(1),
132
+ /** What the event means and when it fires. */
133
+ description: import_zod.z.string().optional(),
134
+ /** Example of the `ctx.event` payload. Documentation, not a validated schema. */
135
+ payload_example: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
136
+ });
137
+ var PiExtensionContributionSchema = import_zod.z.object({
138
+ /** Stable id for this extension within the plugin (`[a-z0-9][a-z0-9._-]*`). */
139
+ id: import_zod.z.string().min(1),
140
+ /** Path to the source, relative to the plugin root: `pi-extensions/<name>.ts`. */
141
+ file: import_zod.z.string().min(1),
142
+ /** Optional one-liner describing what the extension adds to the agent. */
143
+ description: import_zod.z.string().optional()
144
+ });
145
+ var OutputStyleContributionSchema = import_zod.z.object({
146
+ /** Stable id for this style within the plugin (`[a-z0-9][a-z0-9._-]*`). It is
147
+ * also the persisted selection key, so it must survive a settings key and a URL
148
+ * path. */
149
+ id: import_zod.z.string().min(1),
150
+ /** SOURCE form: path to the Markdown file, relative to the plugin root —
151
+ * exactly `output-styles/<name>.md`. `ryu pack` replaces this with `source`. */
152
+ file: import_zod.z.string().min(1).optional(),
153
+ /** WIRE form: the file's contents verbatim, frontmatter INCLUDED. The whole file
154
+ * rather than a pre-split body plus mirrored `name`/`description` keys, so a
155
+ * plugin style and a user's own `output-styles/*.md` go through one parser and
156
+ * the frontmatter stays the single source of truth for a style's metadata. */
157
+ source: import_zod.z.string().optional()
158
+ });
159
+ var DEFAULT_WIDGET_MIME = "text/html+skybridge";
160
+ var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
161
+ var WidgetContributionSchema = import_zod.z.object({
162
+ /** The fully-qualified tool id whose result renders this widget. */
163
+ tool_id: import_zod.z.string().min(1),
164
+ /** `ui://widget/<slug>.html` — the widget resource uri. */
165
+ uri: import_zod.z.string().min(1),
166
+ /** Source entry (e.g. `src/apps/checklist/index.tsx`) for `ryu pack`. */
167
+ ui_entry: import_zod.z.string().optional(),
168
+ /** Widget MIME dialect (default `text/html+skybridge`). */
169
+ mime: import_zod.z.string().default(DEFAULT_WIDGET_MIME),
170
+ /** Default display mode (`inline` | `fullscreen` | `pip`). */
171
+ default_display_mode: import_zod.z.string().default(DEFAULT_WIDGET_DISPLAY_MODE)
172
+ });
173
+ var ChatWidgetTemplateSchema = import_zod.z.object({
174
+ id: import_zod.z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/),
175
+ title: import_zod.z.string().min(1),
176
+ description: import_zod.z.string().optional(),
177
+ triggers: import_zod.z.array(import_zod.z.string()).default([]),
178
+ examples: import_zod.z.array(import_zod.z.string()).default([]),
179
+ backing: import_zod.z.object({
180
+ tool_id: import_zod.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/).optional(),
181
+ view_id: import_zod.z.string().regex(/^[a-z0-9][a-z0-9._:-]*$/).optional()
182
+ }),
183
+ display_mode: import_zod.z.string().min(1),
184
+ safe_action_ids: import_zod.z.array(import_zod.z.string().regex(/^[a-z0-9][a-z0-9._-]*$/)).default([]),
185
+ availability: import_zod.z.string().default("available")
186
+ }).superRefine((value, ctx) => {
187
+ const count = Number(Boolean(value.backing.tool_id)) + Number(Boolean(value.backing.view_id));
188
+ if (count !== 1 && value.availability === "available") {
189
+ ctx.addIssue({
190
+ code: "custom",
191
+ path: ["backing"],
192
+ message: "available templates need exactly one backing tool_id or view_id"
193
+ });
194
+ }
195
+ if (count > 1) {
196
+ ctx.addIssue({
197
+ code: "custom",
198
+ path: ["backing"],
199
+ message: "backing must declare at most one of tool_id or view_id"
200
+ });
201
+ }
202
+ });
203
+ var ToolAppConfigSchema = import_zod.z.object({
204
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>.<name>` id. */
205
+ slug: import_zod.z.string().min(1),
206
+ /** The tool description the model reads when choosing it. Carried here because a
207
+ * packed app's manifest is the only channel (there is no `generated.rs`); Core's
208
+ * app-tool synthesis reads it back onto the `RegistryTool`. */
209
+ description: import_zod.z.string(),
210
+ /** JSON Schema for the tool's arguments (used for validation + the LLM tool
211
+ * surface). Snake_case to match `widget_accessible`. Absent = no arguments. */
212
+ input_schema: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
213
+ /** True when calling this tool renders the app's widget (carries the template). */
214
+ widget: import_zod.z.boolean().default(false),
215
+ /** True when a mounted widget may `callTool` this tool (a companion), or when a
216
+ * render tool's widget may call any companion the app declares. */
217
+ widget_accessible: import_zod.z.boolean().default(false),
218
+ /** Optional status label shown while the render tool runs. */
219
+ invoking: import_zod.z.string().optional(),
220
+ /** Optional status label shown when the render tool finishes. */
221
+ invoked: import_zod.z.string().optional()
222
+ });
223
+ var SlashCommandOptionSchema = import_zod.z.object({
224
+ description: import_zod.z.string().optional(),
225
+ label: import_zod.z.string().min(1),
226
+ value: import_zod.z.string().min(1)
227
+ }).passthrough();
228
+ var SlashCommandCustomOptionSchema = import_zod.z.union([
229
+ import_zod.z.literal(true),
230
+ import_zod.z.object({
231
+ description: import_zod.z.string().optional(),
232
+ label: import_zod.z.string().min(1).optional()
233
+ }).passthrough()
234
+ ]);
235
+ var SlashCommandArgumentSchema = import_zod.z.object({
236
+ allow_custom: import_zod.z.boolean().optional(),
237
+ custom: SlashCommandCustomOptionSchema.optional(),
238
+ description: import_zod.z.string().optional(),
239
+ name: import_zod.z.string().min(1),
240
+ options: import_zod.z.array(SlashCommandOptionSchema).optional()
241
+ }).passthrough();
242
+ var SlashCommandContributionSchema = import_zod.z.object({
243
+ args: import_zod.z.array(SlashCommandArgumentSchema).optional(),
244
+ body: import_zod.z.string().optional(),
245
+ command: import_zod.z.string().min(1),
246
+ description: import_zod.z.string().optional(),
247
+ id: import_zod.z.string().optional(),
248
+ parameters: import_zod.z.array(SlashCommandArgumentSchema).optional()
249
+ }).passthrough();
250
+ var ContributesSchema = import_zod.z.object({
251
+ turn_hooks: import_zod.z.array(TurnHookContributionSchema).default([]),
252
+ /** App events this plugin EMITS — the provider half of the hook system, whose
253
+ * consumer half is `turn_hooks`. Mirrors the Rust `Contributes.hook_events`;
254
+ * omitting it here would have `ryu pack` strip every declared event before
255
+ * signing, leaving an app that emits events nothing is allowed to subscribe to. */
256
+ hook_events: import_zod.z.array(HookEventContributionSchema).default([]),
257
+ composer_controls: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
258
+ /** Chat feature descriptors whose behavior is implemented by the host shell.
259
+ * Mirrors the Rust-side `Contributes.chat_features`; keeping this field in the
260
+ * authoring schema prevents `ryu pack` from silently deleting a plugin's chat
261
+ * feature declaration before signing. */
262
+ chat_features: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
263
+ settings_tabs: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
264
+ slash_commands: import_zod.z.array(SlashCommandContributionSchema).default([]),
265
+ /** App widgets (Ryu Apps). Each binds a render tool id to its
266
+ * `ui://widget/<slug>.html` template. Mirrors the Rust-side
267
+ * `Contributes.widgets` field, without which the CLI's zod parse would strip
268
+ * every widget an app authored here declares. */
269
+ widgets: import_zod.z.array(WidgetContributionSchema).default([]),
270
+ /** Metadata-only chat widget templates. */
271
+ chat_widget_templates: import_zod.z.array(ChatWidgetTemplateSchema).optional(),
272
+ /** App-registered sidebar sections (header + live list) and buttons (single nav
273
+ * rows). Loosely typed here — the shell owns the spec vocabulary — matching how
274
+ * `composer_controls`/`settings_tabs` are declared. Mirrors the Rust-side
275
+ * `Contributes.sidebar_sections` / `Contributes.sidebar_buttons`. */
276
+ sidebar_sections: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
277
+ sidebar_buttons: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
278
+ /** App-registered workspace dock panels (a tab in the desktop's bottom/right
279
+ * dock). Loosely typed for the same reason as the surfaces above — the shell
280
+ * owns the `panel` render-mode vocabulary and the `spec` payload. Mirrors the
281
+ * Rust-side `Contributes.dock_panels`; without it the CLI's zod parse would
282
+ * strip the dock panel an app declares here. */
283
+ dock_panels: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
284
+ /** App-registered live activities (the desktop "Dynamic Island" cards). Loosely
285
+ * typed for the same reason as the surfaces above — the shell owns the
286
+ * `spec` vocabulary. Mirrors the Rust-side `Contributes.live_activities`;
287
+ * without it the CLI's zod parse would strip every live activity an app
288
+ * declares here, so a packed bundle would ship a dock that silently stays
289
+ * empty. */
290
+ live_activities: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
291
+ /** Deletable data categories the app owns — one "Delete all X" row in Settings
292
+ * → Danger Zone. Mirrors the Rust-side `Contributes.data_categories`; without
293
+ * it the CLI's zod parse would strip the declaration before signing, and the
294
+ * app's danger-zone row would simply never appear on any node that installed
295
+ * the packed bundle. Loosely typed here for the same reason as the surfaces
296
+ * above — Core is the layer that types it, because Core is the layer that has
297
+ * to resolve the id to something that can actually delete the rows. */
298
+ data_categories: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
299
+ /** Language servers the plugin declares, keyed by server name — the mirror of
300
+ * Claude Code's `.lsp.json` / `lspServers`, so a config written for either host
301
+ * loads in the other. Mirrors the Rust-side `Contributes.lsp_servers`; without
302
+ * it the CLI's zod parse would strip every language server a plugin declares,
303
+ * before the manifest is signed.
304
+ *
305
+ * The ENTRY is deliberately a loose record and not a 13-field `z.object()`
306
+ * mirroring `LspServerContribution`. Claude Code owns this field vocabulary,
307
+ * not Ryu: a typed object here would strip a field from a newer Claude release
308
+ * on its way through `ryu pack` — the same silent-deletion bug this field
309
+ * exists to fix, one level down. Core is the layer that types it, because Core
310
+ * is the layer that acts on it. */
311
+ lsp_servers: import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default({}),
312
+ /** Pi extensions the plugin ships — TypeScript the managed `ryu` (Pi) agent
313
+ * loads at process start. Mirrors the Rust-side `Contributes.pi_extensions`;
314
+ * without it the CLI's zod parse would strip the declaration before signing,
315
+ * and the packed plugin would ship a `pi-extensions/` folder nothing loads.
316
+ *
317
+ * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
318
+ * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
319
+ * Claude Code's to extend. */
320
+ pi_extensions: import_zod.z.array(PiExtensionContributionSchema).default([]),
321
+ /** Output styles the plugin ships — Markdown files that rewrite the system
322
+ * prompt's voice. Mirrors the Rust-side `Contributes.output_styles`; without it
323
+ * the CLI's zod parse would strip the declaration, and `ryu pack` would sign a
324
+ * bundle whose styles simply do not exist. Worse than the usual case of that
325
+ * bug: the styles' `.md` files are not carried by the bundle either, so there
326
+ * would be no residue to notice — the plugin would install clean and contribute
327
+ * nothing. */
328
+ output_styles: import_zod.z.array(OutputStyleContributionSchema).default([]),
329
+ /** Per-message actions contributed by an enabled plugin. Kept as loose records
330
+ * so renderer-specific `kind`/`args` payloads survive `ryu pack` unchanged. */
331
+ message_actions: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
332
+ /** Buttons contributed to the floating text-selection toolbar. Kept as loose
333
+ * records so host-owned dispatch args survive `ryu pack` unchanged. */
334
+ selection_actions: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([])
335
+ });
336
+ var SetupStepSchema = import_zod.z.object({
337
+ /** Card heading (e.g. the companion app name). */
338
+ title: import_zod.z.string().optional(),
339
+ /** Instruction body shown under the title. */
340
+ description: import_zod.z.string().optional(),
341
+ /** Label for the optional action button. */
342
+ actionLabel: import_zod.z.string().optional(),
343
+ /** URL the action button opens (validated server-side on publish). */
344
+ actionUrl: import_zod.z.string().optional()
345
+ });
346
+ var AppDependencySchema = import_zod.z.object({
347
+ /** The `id` of the plugin this one depends on. */
348
+ id: import_zod.z.string().min(1, "dependency id is required"),
349
+ /** Optional MINIMUM version the dependency must satisfy (`"1.2.0"` = `">=1.2.0"`). */
350
+ min_version: import_zod.z.string().min(1).optional()
351
+ });
352
+ var CapabilityReqSchema = import_zod.z.object({
353
+ /** Capability name (e.g. `"rag"`, `"memory"`, `"tts"`). Matched against a
354
+ * provider's `provides[].capability`. */
355
+ capability: import_zod.z.string().min(1, "capability name is required"),
356
+ /** Optional MINIMUM capability version the bound provider must satisfy
357
+ * (`"1.2.0"` = `">=1.2.0"`). Absent = any version. */
358
+ min_version: import_zod.z.string().min(1).optional()
359
+ });
360
+ var RequiresSchema = import_zod.z.object({
361
+ /** Other plugins that must be installed + enabled before this one enables. */
362
+ apps: import_zod.z.array(AppDependencySchema).default([]),
363
+ /**
364
+ * Abstract capability edges the broker resolves to a bound provider at
365
+ * enable time. Mirrors `Requires::capabilities` in
366
+ * `crates/ryu-kernel-contracts` — an `apps` edge names a specific plugin; a
367
+ * `capabilities` edge names a capability and lets the binding registry choose
368
+ * the provider. Each is lowered to an app-id graph edge once bound, so the
369
+ * enable/disable/cycle machinery is shared. Empty for the common case.
370
+ */
371
+ capabilities: import_zod.z.array(CapabilityReqSchema).default([]),
372
+ /**
373
+ * Permission grants implied by the dependencies. Declaration only — the
374
+ * Gateway remains the sole authority on what a grant *allows*, and Core's
375
+ * dependency graph resolves `apps` only.
376
+ */
377
+ grants: import_zod.z.array(import_zod.z.string()).default([])
378
+ });
379
+ var SurfaceSchema = import_zod.z.enum([
380
+ /** The Ryu Gateway. */
381
+ "gateway",
382
+ /** A headless Core node (no UI). */
383
+ "core",
384
+ /** The Tauri desktop app. */
385
+ "desktop",
386
+ /** The Electron dynamic-island companion. */
387
+ "island",
388
+ /** The Expo/React-Native mobile app. */
389
+ "mobile",
390
+ /** The browser extension. */
391
+ "extension",
392
+ /** The Next.js web app. */
393
+ "web",
394
+ /** The terminal client. */
395
+ "cli"
396
+ ]);
397
+ var EnginesReqSchema = import_zod.z.object({
398
+ /** Floor for the terminal (`cli`) surface. */
399
+ cli: import_zod.z.string().optional(),
400
+ /** Floor for the Tauri desktop app. */
401
+ desktop: import_zod.z.string().optional(),
402
+ /** Floor for the browser extension. */
403
+ extension: import_zod.z.string().optional(),
404
+ /** Floor for the Ryu Gateway. */
405
+ gateway: import_zod.z.string().optional(),
406
+ /** Floor for the dynamic-island companion. */
407
+ island: import_zod.z.string().optional(),
408
+ /** Floor for the mobile app. */
409
+ mobile: import_zod.z.string().optional(),
410
+ /** Floor for the running **Core** (e.g. `">=0.3.0"`). Required. */
411
+ ryu: import_zod.z.string().min(1, "engines.ryu is required when engines is present"),
412
+ /** Floor for the Next.js web app. */
413
+ web: import_zod.z.string().optional()
414
+ });
415
+ var McpServerAuthSchema = import_zod.z.object({
416
+ client_id: import_zod.z.string().min(1).optional(),
417
+ type: import_zod.z.literal("oauth")
418
+ }).strict();
419
+ var McpServerDeclSchema = import_zod.z.object({
420
+ args: import_zod.z.array(import_zod.z.string()).default([]),
421
+ auth: McpServerAuthSchema.optional(),
422
+ command: import_zod.z.string().optional(),
423
+ command_env: import_zod.z.string().optional(),
424
+ description: import_zod.z.string().optional(),
425
+ enabled: import_zod.z.boolean().default(true),
426
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}),
427
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).default({}),
428
+ type: import_zod.z.enum(["stdio", "http", "streamable-http", "streamable_http", "sse"]).optional(),
429
+ url: import_zod.z.url().optional()
430
+ }).superRefine((server, context) => {
431
+ if (!(server.command || server.url)) {
432
+ context.addIssue({
433
+ code: "custom",
434
+ message: "an MCP server requires command or url"
435
+ });
436
+ }
437
+ if (!server.auth) {
438
+ return;
439
+ }
440
+ if (server.command || server.type === "stdio" || !server.url) {
441
+ context.addIssue({
442
+ code: "custom",
443
+ message: "OAuth is supported only for remote HTTP MCP servers"
444
+ });
445
+ return;
446
+ }
447
+ if (Object.keys(server.headers).some(
448
+ (name) => name.toLowerCase() === "authorization"
449
+ )) {
450
+ context.addIssue({
451
+ code: "custom",
452
+ message: "OAuth cannot be combined with a static Authorization header"
453
+ });
454
+ }
455
+ const url = new URL(server.url);
456
+ const loopback = url.hostname === "localhost" || url.hostname.startsWith("127.") || url.hostname === "[::1]" || url.hostname === "::1";
457
+ if (url.username || url.password || url.hash) {
458
+ context.addIssue({
459
+ code: "custom",
460
+ message: "OAuth MCP URLs cannot contain credentials or fragments"
461
+ });
462
+ }
463
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
464
+ context.addIssue({
465
+ code: "custom",
466
+ message: "OAuth MCP URLs must use HTTPS except on loopback"
467
+ });
468
+ }
469
+ });
470
+ var PluginManifestSchema = import_zod.z.object({
471
+ /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
472
+ id: import_zod.z.string().min(1, "id is required"),
473
+ /** Human-readable display name shown in the plugin store / launcher. */
474
+ name: import_zod.z.string().min(1, "name is required"),
475
+ /**
476
+ * Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
477
+ * whose version is not valid semver; the regex here enforces the same rule at
478
+ * SDK-build time.
479
+ */
480
+ version: import_zod.z.string().regex(
481
+ /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
482
+ "version must be a valid semver string (e.g. 1.0.0)"
483
+ ),
484
+ /** Core-owned release maturity metadata. The Rust contract validates the
485
+ * richer shape; the SDK authoring parser must preserve it for pack/publish. */
486
+ stability: import_zod.z.unknown().optional(),
487
+ /**
488
+ * Lower-case hex `sha256(utf8_bytes(ui_code))` binding the plugin's bundled
489
+ * sandboxed-UI code to this manifest. `ryu pack` / `ryu publish` compute it and
490
+ * write it here BEFORE the manifest is signed, so the hash rides INSIDE the
491
+ * Gateway-signed surface while the `ui_code` blob rides OUTSIDE it as payload;
492
+ * Core's install path recomputes the hash over the fetched code and rejects a
493
+ * mismatch fail-closed. Absent for a manifest-only plugin (no bundled UI).
494
+ * Mirrors Core's `PluginManifest.ui_code_sha256`.
495
+ */
496
+ ui_code_sha256: import_zod.z.string().nullish(),
497
+ /** The Runnables this plugin bundles. */
498
+ runnables: import_zod.z.array(RunnableMetaSchema).default([]),
499
+ /**
500
+ * Permission grants this plugin declares it needs (e.g. `"mcp:web_search"`).
501
+ * Declarations only — grant enforcement is the Gateway's responsibility.
502
+ */
503
+ permission_grants: import_zod.z.array(import_zod.z.string()).default([]),
504
+ /** Deny-by-default sandbox permissions. Core remains authoritative; this
505
+ * schema mirrors the fields so `ryu pack` cannot strip them. */
506
+ permissions: import_zod.z.object({
507
+ fs: import_zod.z.object({
508
+ read: import_zod.z.array(import_zod.z.string()).default([]),
509
+ write: import_zod.z.array(import_zod.z.string()).default([])
510
+ }).optional(),
511
+ child_process: import_zod.z.boolean().optional(),
512
+ run: import_zod.z.array(import_zod.z.string()).default([]),
513
+ network: import_zod.z.union([import_zod.z.boolean(), import_zod.z.array(import_zod.z.string())]).optional(),
514
+ tool: import_zod.z.array(import_zod.z.string()).default([])
515
+ }).optional(),
516
+ /** Core-owned permission presentation levels. Preserved verbatim here and
517
+ * validated by Core's authoritative manifest contract. */
518
+ permission_levels: import_zod.z.unknown().optional(),
519
+ /** Remote or stdio MCP servers registered by this plugin. */
520
+ mcp_servers: import_zod.z.record(import_zod.z.string(), McpServerDeclSchema).optional(),
521
+ /** Core-owned sidecar declarations. Their full process/HTTP schema stays in
522
+ * the Rust contract; this authoring layer must never strip them. */
523
+ sidecars: import_zod.z.unknown().optional(),
524
+ /**
525
+ * Optional Companion surface (an in-desktop overlay or sidebar panel).
526
+ * Absent when the plugin has no Companion surface.
527
+ */
528
+ companion: CompanionSurfaceSchema.optional(),
529
+ /**
530
+ * VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
531
+ * `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
532
+ * enabled flag, so `["*"]` is the usual value.
533
+ */
534
+ activation_events: import_zod.z.array(import_zod.z.string()).default([]),
535
+ /**
536
+ * Contribution points: server-side turn hooks + declarative UI widgets.
537
+ * Absent for a plugin that contributes nothing here.
538
+ */
539
+ contributes: ContributesSchema.optional(),
540
+ /**
541
+ * **Plugin-to-plugin dependencies** — the other plugins this one needs. Core
542
+ * resolves them into a topological enable order (dependencies enable first;
543
+ * disabling one is refused while an enabled dependent needs it).
544
+ *
545
+ * Absent = **no dependencies**, the backward-compatible default. Kept
546
+ * `.optional()` (never defaulted) so a manifest that declares none serialises
547
+ * with no `requires` key at all, exactly like Core's
548
+ * `#[serde(skip_serializing_if = "Option::is_none")]`.
549
+ */
550
+ requires: RequiresSchema.optional(),
551
+ /**
552
+ * Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
553
+ * surface** — the backward-compatible default, which must never be read as
554
+ * "runs nowhere". Core filters only when the list is explicitly non-empty, and
555
+ * only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
556
+ * an unsupported-target plugin stays installable and inspectable.
557
+ */
558
+ targets: import_zod.z.array(SurfaceSchema).default([]),
559
+ /** Rich per-surface declarations (`support`, UI, contributed commands, …).
560
+ * Core owns and validates the nested vocabulary. */
561
+ surfaces: import_zod.z.unknown().optional(),
562
+ /**
563
+ * Host version floors — the semver requirement each surface must satisfy for
564
+ * this plugin to install. Mirrors Core's `EnginesReq`
565
+ * (`crates/core/kernel-contracts/src/manifest.rs`).
566
+ *
567
+ * `ryu` is the **Core** floor and the only required key (it is the legacy
568
+ * spelling; every manifest in the wild carries just that one). The rest are
569
+ * optional per-surface floors.
570
+ *
571
+ * REGRESSION THIS FIXES: `engines` was absent from this schema entirely, and
572
+ * zod strips unlisted keys — so `ryu pack` silently dropped the whole block
573
+ * from every bundle it produced. A plugin could declare a Core floor, publish,
574
+ * and ship a bundle that declared none. Any new host floor MUST be added here
575
+ * as well as in the Rust contract, or it does not survive packing.
576
+ */
577
+ engines: EnginesReqSchema.optional(),
578
+ /**
579
+ * Optional per-item AFFILIATE terms: the commission paid to a referrer when a
580
+ * referred user buys this (paid) item. `value` is basis points for `percent`
581
+ * (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
582
+ * falls back to the seller org owner's default affiliate terms. This is the
583
+ * authoring surface for the marketplace publish body's `affiliate` field (the
584
+ * server re-validates it); it only takes effect on a paid item.
585
+ */
586
+ affiliate: import_zod.z.object({
587
+ enabled: import_zod.z.boolean().default(false),
588
+ rule: import_zod.z.object({
589
+ type: import_zod.z.enum(["percent", "flat"]),
590
+ value: import_zod.z.number().nonnegative(),
591
+ recurring: import_zod.z.boolean().default(false),
592
+ durationMonths: import_zod.z.number().int().positive().nullish(),
593
+ fundedBy: import_zod.z.enum(["platform", "seller"]).default("platform")
594
+ }).optional()
595
+ }).optional(),
596
+ // ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
597
+ // Optional store-listing fields a plugin author declares so the marketplace
598
+ // detail dialog renders a richer App-Store-style preview. Field names align
599
+ // with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
600
+ // one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
601
+ // rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
602
+ // body (not inside the signed manifest blob) so the control plane stores them.
603
+ // All optional + additive: a manifest omitting them still validates.
604
+ /** Longer plain/markdown description shown in the detail dialog. */
605
+ description: import_zod.z.string().optional(),
606
+ /** Short one-line pitch shown under the name (Ryu extension). */
607
+ tagline: import_zod.z.string().optional(),
608
+ /**
609
+ * Publisher identity. A bare string OR a Claude-style object; `ryu publish`
610
+ * resolves it to the display `developer` (`author.name` when an object).
611
+ */
612
+ author: import_zod.z.union([
613
+ import_zod.z.string(),
614
+ import_zod.z.object({
615
+ name: import_zod.z.string(),
616
+ email: import_zod.z.string().optional(),
617
+ url: import_zod.z.string().optional()
618
+ })
619
+ ]).optional(),
620
+ /** Public source repository URL (Claude/Codex `repository`). */
621
+ repository: import_zod.z.string().url().optional(),
622
+ /** True when the provider operates outside the local Ryu runtime. */
623
+ external: import_zod.z.boolean().optional(),
624
+ /** Project/marketing homepage — maps to the listing `website` (Claude field). */
625
+ homepage: import_zod.z.string().optional(),
626
+ /** Free-text search keywords (Claude field). */
627
+ keywords: import_zod.z.array(import_zod.z.string()).optional(),
628
+ /** Stable Marketplace filter labels (Ryu extension). */
629
+ tags: import_zod.z.array(import_zod.z.string()).optional(),
630
+ /** Taxonomy category beyond the runnable kinds (Claude field). */
631
+ category: import_zod.z.string().optional(),
632
+ /** SPDX-ish license identifier (Claude field). */
633
+ license: import_zod.z.string().optional(),
634
+ /** Square logo/icon URL for the listing card + detail header. */
635
+ iconUrl: import_zod.z.string().optional(),
636
+ /**
637
+ * Icon-primitive id for the listing card (Ryu extension): an Iconify/icons0
638
+ * `prefix:name`, a bare Hugeicons name, or a URL, resolved by the shared `Icon`
639
+ * primitive. A monochrome GLYPH masked with the current text colour — distinct
640
+ * from `iconUrl` (a raster logo). Falls back to `iconUrl` when omitted.
641
+ */
642
+ icon: import_zod.z.string().optional(),
643
+ /** Optional detail-page hero banner metadata forwarded to the marketplace. */
644
+ banner: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
645
+ /**
646
+ * Dithered-gradient background for the card's icon square (Ryu extension),
647
+ * mirroring dither-kit's `DitherGradient` props. `from`/`to` are a palette-colour
648
+ * name (`green`, `blue`, `purple`, `pink`, `orange`, `red`, `grey`) or a hue
649
+ * number (0–360); `direction` is where `to` ends up. Renders behind the glyph in
650
+ * place of a flat `iconBackground`; the render layer validates + falls back.
651
+ */
652
+ iconDither: import_zod.z.object({
653
+ from: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]),
654
+ to: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional(),
655
+ direction: import_zod.z.enum(["up", "down", "left", "right"]).optional()
656
+ }).optional(),
657
+ /** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
658
+ screenshots: import_zod.z.array(import_zod.z.string()).optional(),
659
+ /** Privacy policy URL surfaced on detail (Ryu extension). */
660
+ privacyPolicyUrl: import_zod.z.string().optional(),
661
+ /** Terms-of-service URL surfaced on detail (Ryu extension). */
662
+ termsOfServiceUrl: import_zod.z.string().optional(),
663
+ /**
664
+ * Human-readable capability strings (Ryu extension). When omitted the control
665
+ * plane derives a default from `permission_grants`, so declaring this is only
666
+ * needed to override the derived labels.
667
+ */
668
+ capabilities: import_zod.z.array(import_zod.z.string()).optional(),
669
+ /** Example prompt chips shown on detail (Ryu extension). */
670
+ examplePrompts: import_zod.z.array(import_zod.z.string()).optional(),
671
+ /**
672
+ * Optional companion/config card (Ryu extension): a single setup step or an
673
+ * array of steps guiding the user through post-install configuration.
674
+ */
675
+ setup: import_zod.z.union([SetupStepSchema, import_zod.z.array(SetupStepSchema)]).optional()
676
+ }).passthrough().superRefine((manifest, context) => {
677
+ const hasOAuthServer = Object.values(manifest.mcp_servers ?? {}).some(
678
+ (server) => server.auth?.type === "oauth"
679
+ );
680
+ if (!hasOAuthServer) {
681
+ return;
682
+ }
683
+ for (const grant of ["mcp:server", "identity.read"]) {
684
+ if (!manifest.permission_grants.includes(grant)) {
685
+ context.addIssue({
686
+ code: "custom",
687
+ message: `OAuth MCP servers require the ${grant} permission grant`,
688
+ path: ["permission_grants"]
689
+ });
690
+ }
691
+ }
692
+ });
693
+
694
+ // src/runnable/tool.ts
695
+ function validateInput(input, schema, toolId) {
696
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
697
+ throw new Error(
698
+ `[ryu-sdk] Tool "${toolId}" input must be an object, got ${JSON.stringify(input)}`
699
+ );
700
+ }
701
+ const record = input;
702
+ for (const requiredKey of schema.required ?? []) {
703
+ if (!(requiredKey in record)) {
704
+ throw new Error(
705
+ `[ryu-sdk] Tool "${toolId}" input missing required field "${requiredKey}"`
706
+ );
707
+ }
708
+ }
709
+ for (const [key, prop] of Object.entries(schema.properties)) {
710
+ if (!(key in record)) {
711
+ continue;
712
+ }
713
+ const value = record[key];
714
+ if (!checkType(value, prop.type)) {
715
+ throw new Error(
716
+ `[ryu-sdk] Tool "${toolId}" input field "${key}" expected type "${prop.type}", got ${JSON.stringify(value)}`
717
+ );
718
+ }
719
+ }
720
+ }
721
+ function checkType(value, type) {
722
+ switch (type) {
723
+ case "string":
724
+ return typeof value === "string";
725
+ case "number":
726
+ case "integer":
727
+ return typeof value === "number";
728
+ case "boolean":
729
+ return typeof value === "boolean";
730
+ case "array":
731
+ return Array.isArray(value);
732
+ case "object":
733
+ return typeof value === "object" && value !== null && !Array.isArray(value);
734
+ default:
735
+ return false;
736
+ }
737
+ }
738
+ function defineTool(options) {
739
+ const { description, id, name, schema, run } = options;
740
+ const code = `return await (${run.toString()})(input, host);`;
741
+ return {
742
+ id,
743
+ name,
744
+ kind: "tool",
745
+ schema,
746
+ ...description === void 0 ? {} : { description },
747
+ code,
748
+ run(input, ctx) {
749
+ validateInput(input, schema, id);
750
+ return run(input, ctx);
751
+ }
752
+ };
753
+ }
754
+ function inlineToolRunnable(tool, options) {
755
+ return {
756
+ id: tool.id,
757
+ name: tool.name,
758
+ kind: "tool",
759
+ config: {
760
+ slug: tool.id,
761
+ backend: "inline_deno",
762
+ code: tool.code,
763
+ input_schema: tool.schema,
764
+ ...options?.description === void 0 ? tool.description === void 0 ? {} : { description: tool.description } : { description: options.description },
765
+ ...options?.outputSchema ? { output_schema: options.outputSchema } : {},
766
+ ...options?.annotations ? { annotations: options.annotations } : {},
767
+ ...options?.action === void 0 ? {} : { action: options.action },
768
+ ...options?.needsApproval === void 0 ? {} : { needs_approval: options.needsApproval }
769
+ }
770
+ };
771
+ }
772
+
773
+ // src/runnable/action.ts
774
+ function deriveAnnotations(effect, annotations) {
775
+ const resolved = {
776
+ ...annotations,
777
+ readOnlyHint: annotations?.readOnlyHint ?? effect === "read",
778
+ destructiveHint: annotations?.destructiveHint ?? effect === "mutate"
779
+ };
780
+ if (effect === "read" && resolved.destructiveHint) {
781
+ throw new Error(
782
+ "[ryu-sdk] read actions cannot set annotations.destructiveHint=true"
783
+ );
784
+ }
785
+ if (effect === "mutate" && resolved.readOnlyHint) {
786
+ throw new Error(
787
+ "[ryu-sdk] mutate actions cannot set annotations.readOnlyHint=true"
788
+ );
789
+ }
790
+ return resolved;
791
+ }
792
+ function actionManifestEntry(action) {
793
+ const options = {
794
+ action: true,
795
+ annotations: action.annotations,
796
+ description: action.description,
797
+ needsApproval: action.needsApproval,
798
+ ...action.outputSchema ? { outputSchema: action.outputSchema } : {}
799
+ };
800
+ return inlineToolRunnable(action, options);
801
+ }
802
+ function actionToManifest(action, options) {
803
+ const grants = [.../* @__PURE__ */ new Set([...options.grants ?? [], "tool:execute"])];
804
+ const raw = {
805
+ id: options.id,
806
+ name: options.name ?? action.name,
807
+ version: options.version,
808
+ runnables: [actionManifestEntry(action)],
809
+ permission_grants: grants,
810
+ activation_events: [...options.activationEvents ?? ["*"]],
811
+ targets: [...options.targets ?? []]
812
+ };
813
+ const result = PluginManifestSchema.safeParse(raw);
814
+ if (!result.success) {
815
+ const first = result.error.issues[0];
816
+ const field = first?.path.join(".") ?? "unknown";
817
+ const message = first?.message ?? "validation failed";
818
+ throw new Error(
819
+ `[ryu-sdk] action manifest validation failed at '${field}': ${message}`
820
+ );
821
+ }
822
+ return result.data;
823
+ }
824
+ function defineAction(options) {
825
+ const annotations = deriveAnnotations(options.effect, options.annotations);
826
+ const tool = defineTool({
827
+ description: options.description,
828
+ id: options.id,
829
+ name: options.name,
830
+ run: options.run,
831
+ schema: options.schema
832
+ });
833
+ const action = {
834
+ ...tool,
835
+ action: true,
836
+ annotations,
837
+ description: options.description,
838
+ effect: options.effect,
839
+ needsApproval: options.needsApproval ?? false,
840
+ ...options.outputSchema ? { outputSchema: options.outputSchema } : {},
841
+ toManifest(manifestOptions) {
842
+ return actionToManifest(action, manifestOptions);
843
+ },
844
+ toMcpTool(context) {
845
+ return {
846
+ name: action.id,
847
+ description: action.description,
848
+ inputSchema: action.schema,
849
+ run: (input) => action.run(input, context)
850
+ };
851
+ }
852
+ };
853
+ return action;
854
+ }
855
+
856
+ // src/runnable/agent.ts
857
+ var CAPABILITY_SLOTS = ["rag", "memory", "tts", "stt"];
858
+ function lowerChatSlot(chat) {
859
+ if (chat === void 0) {
860
+ return {};
861
+ }
862
+ if (typeof chat === "string") {
863
+ return { model: chat };
864
+ }
865
+ return {
866
+ model: chat.model,
867
+ engine: chat.engine,
868
+ persona: chat.persona,
869
+ modelPrefKey: chat.modelPrefKey
870
+ };
871
+ }
872
+ function lowerCapabilitySlot(capability, slot, card) {
873
+ if (slot === void 0 || slot === false) {
874
+ return;
875
+ }
876
+ if (slot === true) {
877
+ card.capabilities.push({ capability });
878
+ return;
879
+ }
880
+ if (typeof slot === "string") {
881
+ card.capabilities.push({ capability });
882
+ card.providers[capability] = slot;
883
+ return;
884
+ }
885
+ card.capabilities.push(
886
+ slot.minVersion ? { capability, min_version: slot.minVersion } : { capability }
887
+ );
888
+ if (slot.provider) {
889
+ card.providers[capability] = slot.provider;
890
+ }
891
+ }
892
+ function lowerSlots(options) {
893
+ const card = {
894
+ ...lowerChatSlot(options.chat),
895
+ capabilities: [],
896
+ providers: {},
897
+ tools: []
898
+ };
899
+ for (const capability of CAPABILITY_SLOTS) {
900
+ lowerCapabilitySlot(capability, options[capability], card);
901
+ }
902
+ for (const t of options.tools ?? []) {
903
+ card.tools.push(typeof t === "string" ? t : t.id);
904
+ }
905
+ return card;
906
+ }
907
+ function defaultRun(card) {
908
+ return async (input, ctx) => {
909
+ const content = typeof input === "string" ? input : JSON.stringify(input ?? "");
910
+ const messages = card.persona ? [
911
+ { role: "system", content: card.persona },
912
+ { role: "user", content }
913
+ ] : [{ role: "user", content }];
914
+ const result = await ctx.gateway.chat(messages);
915
+ return result.content;
916
+ };
917
+ }
918
+ function cardConfig(card) {
919
+ const config = {};
920
+ if (card.model) {
921
+ config.model = card.model;
922
+ }
923
+ if (card.engine) {
924
+ config.engine = card.engine;
925
+ }
926
+ if (card.persona) {
927
+ config.persona = card.persona;
928
+ }
929
+ if (card.modelPrefKey) {
930
+ config.model_pref_key = card.modelPrefKey;
931
+ }
932
+ if (Object.keys(card.providers).length > 0) {
933
+ config.capability_providers = card.providers;
934
+ }
935
+ if (card.tools.length > 0) {
936
+ config.tools = card.tools;
937
+ }
938
+ return config;
939
+ }
940
+ function agentToManifest(agent, options) {
941
+ const config = cardConfig(agent.card);
942
+ const meta = {
943
+ id: agent.id,
944
+ name: agent.name,
945
+ kind: "agent",
946
+ ...Object.keys(config).length > 0 ? { config } : {}
947
+ };
948
+ const hasRequires = agent.card.capabilities.length > 0 || (options.grants?.length ?? 0) > 0;
949
+ const raw = {
950
+ id: options.id,
951
+ name: options.name ?? agent.name,
952
+ version: options.version,
953
+ runnables: [meta],
954
+ ...hasRequires ? {
955
+ requires: {
956
+ apps: [],
957
+ capabilities: agent.card.capabilities,
958
+ grants: options.grants ?? []
959
+ }
960
+ } : {}
961
+ };
962
+ const result = PluginManifestSchema.safeParse(raw);
963
+ if (!result.success) {
964
+ const first = result.error.issues[0];
965
+ const field = first?.path.join(".") ?? "unknown";
966
+ const message = first?.message ?? "validation failed";
967
+ throw new Error(
968
+ `agent manifest validation failed at '${field}': ${message}`
969
+ );
970
+ }
971
+ return result.data;
972
+ }
973
+ function defineAgent(options) {
974
+ const { id, name } = options;
975
+ const card = lowerSlots(options);
976
+ const run = options.run ?? defaultRun(card);
977
+ return {
978
+ id,
979
+ name,
980
+ kind: "agent",
981
+ run,
982
+ card,
983
+ toManifest(manifestOptions) {
984
+ return agentToManifest(this, manifestOptions);
985
+ }
986
+ };
987
+ }
988
+
989
+ // src/runnable/app.ts
990
+ var DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
991
+ var DEFAULT_APP_DISPLAY_MODE = "inline";
992
+ var WIDGET_RENDER_GRANT = "widget:render";
993
+ function withWidgetRenderGrant(grants, widgets) {
994
+ const out = [...grants];
995
+ if (widgets.length > 0 && !out.includes(WIDGET_RENDER_GRANT)) {
996
+ out.push(WIDGET_RENDER_GRANT);
997
+ }
998
+ return out;
999
+ }
1000
+ function withToolExecuteGrant(grants, tools) {
1001
+ const out = [...grants];
1002
+ if (tools.some((tool) => tool.runnable !== void 0) && !out.includes("tool:execute")) {
1003
+ out.push("tool:execute");
1004
+ }
1005
+ return out;
1006
+ }
1007
+ function appToolId(server, name) {
1008
+ return `${server}.${name}`;
1009
+ }
1010
+ function defineApp(options) {
1011
+ const server = options.server ?? options.slug;
1012
+ const uri = `ui://widget/${options.slug}.html`;
1013
+ const mime = options.mime ?? DEFAULT_APP_WIDGET_MIME;
1014
+ const displayMode = options.displayMode ?? DEFAULT_APP_DISPLAY_MODE;
1015
+ const hasCompanions = options.tools.some((t) => t.accessible === true);
1016
+ const runnables = [];
1017
+ const widgets = [];
1018
+ for (const spec of options.tools) {
1019
+ const isRender = spec.accessible !== true;
1020
+ const id = appToolId(server, spec.name);
1021
+ const executable = spec.runnable ? inlineToolRunnable(spec.runnable) : void 0;
1022
+ const config = {
1023
+ ...executable?.config ?? {},
1024
+ slug: id,
1025
+ description: spec.description,
1026
+ widget: isRender,
1027
+ widget_accessible: isRender ? hasCompanions : true,
1028
+ ...spec.inputSchema ? { input_schema: spec.inputSchema } : {},
1029
+ ...spec.invoking ? { invoking: spec.invoking } : {},
1030
+ ...spec.invoked ? { invoked: spec.invoked } : {}
1031
+ };
1032
+ runnables.push({
1033
+ id,
1034
+ name: spec.name,
1035
+ kind: "tool",
1036
+ config
1037
+ });
1038
+ if (isRender) {
1039
+ widgets.push({
1040
+ tool_id: id,
1041
+ uri,
1042
+ ui_entry: options.uiEntry,
1043
+ mime,
1044
+ default_display_mode: displayMode
1045
+ });
1046
+ }
1047
+ }
1048
+ const contributes = {
1049
+ turn_hooks: [],
1050
+ chat_features: [],
1051
+ // This builder synthesises an app from its runnables; an app that emits
1052
+ // events declares them in a hand-authored `manifest.json`, same as
1053
+ // `lsp_servers` below.
1054
+ hook_events: [],
1055
+ composer_controls: [],
1056
+ settings_tabs: [],
1057
+ slash_commands: options.slashCommands ?? [],
1058
+ sidebar_sections: [],
1059
+ sidebar_buttons: [],
1060
+ dock_panels: [],
1061
+ live_activities: [],
1062
+ // Empty for the same reason as every sibling family above: this builder
1063
+ // synthesises `widgets` from the app's own runnables and nothing else, and
1064
+ // takes no `contributes` passthrough. An app that wants to declare language
1065
+ // servers writes them in a hand-authored `manifest.json`.
1066
+ lsp_servers: {},
1067
+ // Same reason again: a danger-zone category, a Pi extension and an output
1068
+ // style are all hand-authored declarations, not something derivable from
1069
+ // runnables. An output style in particular points at a Markdown file next
1070
+ // to the manifest, which this builder never writes.
1071
+ data_categories: [],
1072
+ pi_extensions: [],
1073
+ output_styles: [],
1074
+ message_actions: [],
1075
+ selection_actions: [],
1076
+ widgets
1077
+ };
1078
+ const raw = {
1079
+ id: options.id,
1080
+ name: options.title,
1081
+ version: options.version,
1082
+ runnables,
1083
+ // An app that synthesises widgets MUST hold `widget:render`, so this
1084
+ // builder declares it rather than leaving the author to discover it.
1085
+ //
1086
+ // Core gates widget promotion on declared-AND-enabled-AND-granted, and a
1087
+ // missing grant fails as `DeniedNoGrant` — which is an `info!` log and
1088
+ // nothing else. The widget silently renders as plain text, with no error
1089
+ // in the UI and nothing pointing at the manifest.
1090
+ //
1091
+ // Added only when there is a widget to render, and unioned rather than
1092
+ // overwritten so an author's own `grants` list survives and re-declaring
1093
+ // it is not an error.
1094
+ permission_grants: withToolExecuteGrant(
1095
+ withWidgetRenderGrant(options.grants ?? [], widgets),
1096
+ options.tools
1097
+ ),
1098
+ activation_events: options.activationEvents ?? ["*"],
1099
+ contributes,
1100
+ // `targets: []` means EVERY surface, so an app that declares none is
1101
+ // unrestricted — the backward-compatible default.
1102
+ targets: options.targets ?? [],
1103
+ // `requires` stays ABSENT (not `{apps:[],grants:[]}`) when undeclared, so the
1104
+ // emitted manifest carries no key at all — matching Core's
1105
+ // `Option<Requires>` + `skip_serializing_if = "Option::is_none"`.
1106
+ ...options.requires ? {
1107
+ requires: {
1108
+ apps: options.requires.apps ?? [],
1109
+ capabilities: options.requires.capabilities ?? [],
1110
+ grants: options.requires.grants ?? []
1111
+ }
1112
+ } : {}
1113
+ };
1114
+ const result = PluginManifestSchema.safeParse(raw);
1115
+ if (!result.success) {
1116
+ const first = result.error.issues[0];
1117
+ const field = first?.path.join(".") ?? "unknown";
1118
+ const message = first?.message ?? "validation failed";
1119
+ throw new Error(
1120
+ `manifest.json validation failed at '${field}': ${message}`
1121
+ );
1122
+ }
1123
+ return result.data;
1124
+ }
1125
+
1126
+ // src/model/gateway.ts
1127
+ var native = __toESM(require("@ryuhq/sdk-native"), 1);
1128
+ function assertAllowedEgressUrl(baseUrl) {
1129
+ native.assertAllowedEgress(baseUrl);
1130
+ }
1131
+
1132
+ // src/runnable/primitives.ts
1133
+ function dataUrlToBytes(dataUrl) {
1134
+ const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
1135
+ if (!match) {
1136
+ throw new Error(
1137
+ "stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
1138
+ );
1139
+ }
1140
+ const mediaType = match[1] || "application/octet-stream";
1141
+ const isBase64 = Boolean(match[2]);
1142
+ const payload = match[3] ?? "";
1143
+ if (isBase64) {
1144
+ const binary = atob(payload);
1145
+ const bytes = new Uint8Array(binary.length);
1146
+ for (let i = 0; i < binary.length; i++) {
1147
+ bytes[i] = binary.charCodeAt(i);
1148
+ }
1149
+ return { bytes, mediaType };
1150
+ }
1151
+ return {
1152
+ bytes: new TextEncoder().encode(decodeURIComponent(payload)),
1153
+ mediaType
1154
+ };
1155
+ }
1156
+ function bytesToDataUrl(bytes, mediaType) {
1157
+ let binary = "";
1158
+ const chunk = 32768;
1159
+ for (let i = 0; i < bytes.length; i += chunk) {
1160
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
1161
+ }
1162
+ return `data:${mediaType};base64,${btoa(binary)}`;
1163
+ }
1164
+ function httpPrimitiveTransport(options) {
1165
+ const base = options.nodeUrl.replace(/\/+$/, "");
1166
+ assertAllowedEgressUrl(base);
1167
+ const doFetch = options.fetchImpl ?? fetch;
1168
+ const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
1169
+ const failDetail = async (path, res) => {
1170
+ const detail = await res.text().catch(() => "");
1171
+ return new Error(
1172
+ `Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
1173
+ );
1174
+ };
1175
+ const post = async (path, body) => {
1176
+ const res = await doFetch(`${base}${path}`, {
1177
+ method: "POST",
1178
+ headers: { "content-type": "application/json", ...authHeader() },
1179
+ body: JSON.stringify(body ?? {})
1180
+ });
1181
+ if (!res.ok) {
1182
+ throw await failDetail(path, res);
1183
+ }
1184
+ const text = await res.text();
1185
+ return text ? JSON.parse(text) : void 0;
1186
+ };
1187
+ const transcribeDirect = async (body) => {
1188
+ const input = body ?? {};
1189
+ if (!input.audio) {
1190
+ throw new Error(
1191
+ "stt.transcribe requires an `audio` data: URL (the recorded audio)"
1192
+ );
1193
+ }
1194
+ const { bytes, mediaType } = dataUrlToBytes(input.audio);
1195
+ const form = new FormData();
1196
+ form.append(
1197
+ "file",
1198
+ new Blob([bytes], { type: mediaType || "audio/wav" }),
1199
+ input.filename ?? "recording.wav"
1200
+ );
1201
+ const res = await doFetch(`${base}/api/voice/transcribe`, {
1202
+ method: "POST",
1203
+ headers: authHeader(),
1204
+ body: form
1205
+ });
1206
+ if (!res.ok) {
1207
+ throw await failDetail("/api/voice/transcribe", res);
1208
+ }
1209
+ const parsed = await res.json();
1210
+ return (parsed.text ?? "").trim();
1211
+ };
1212
+ const speakDirect = async (body) => {
1213
+ const res = await doFetch(`${base}/api/voice/speak`, {
1214
+ method: "POST",
1215
+ headers: { "content-type": "application/json", ...authHeader() },
1216
+ body: JSON.stringify(body ?? {})
1217
+ });
1218
+ if (!res.ok) {
1219
+ throw await failDetail("/api/voice/speak", res);
1220
+ }
1221
+ const bytes = new Uint8Array(await res.arrayBuffer());
1222
+ const mediaType = res.headers.get("content-type") || "audio/wav";
1223
+ return bytesToDataUrl(bytes, mediaType);
1224
+ };
1225
+ const generateImageDirect = async (body) => {
1226
+ const res = await doFetch(`${base}/api/images/generate`, {
1227
+ method: "POST",
1228
+ headers: { "content-type": "application/json", ...authHeader() },
1229
+ body: JSON.stringify(body ?? {})
1230
+ });
1231
+ if (!res.ok) {
1232
+ throw await failDetail("/api/images/generate", res);
1233
+ }
1234
+ const parsed = await res.json();
1235
+ return (parsed.data ?? []).map(
1236
+ (item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
1237
+ );
1238
+ };
1239
+ return {
1240
+ bridge(method, args) {
1241
+ if (!options.pluginId) {
1242
+ return Promise.reject(
1243
+ new Error(
1244
+ `bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
1245
+ )
1246
+ );
1247
+ }
1248
+ return post(`/api/plugins/${options.pluginId}/host`, { method, args });
1249
+ },
1250
+ direct(path, body) {
1251
+ if (path === "/api/voice/transcribe") {
1252
+ return transcribeDirect(body);
1253
+ }
1254
+ if (path === "/api/voice/speak") {
1255
+ return speakDirect(body);
1256
+ }
1257
+ if (path === "/api/images/generate") {
1258
+ return generateImageDirect(body);
1259
+ }
1260
+ return post(path, body);
1261
+ },
1262
+ capability(cap, body) {
1263
+ return post(`/api/host/capability/${cap}`, body);
1264
+ }
1265
+ };
1266
+ }
1267
+ var PRIMITIVE_BINDINGS = {
1268
+ // Bridge families (existing `PluginHookBridge`).
1269
+ "engines.complete": {
1270
+ transport: "bridge",
1271
+ method: "model.complete",
1272
+ grant: "hook:side-model"
1273
+ },
1274
+ "background.list": {
1275
+ transport: "bridge",
1276
+ method: "background.list",
1277
+ grant: "background:control"
1278
+ },
1279
+ "background.stop": {
1280
+ transport: "bridge",
1281
+ method: "background.stop",
1282
+ grant: "background:control"
1283
+ },
1284
+ "storage.get": {
1285
+ transport: "bridge",
1286
+ method: "storage.get",
1287
+ grant: "storage:kv"
1288
+ },
1289
+ "storage.set": {
1290
+ transport: "bridge",
1291
+ method: "storage.set",
1292
+ grant: "storage:kv"
1293
+ },
1294
+ "storage.delete": {
1295
+ transport: "bridge",
1296
+ method: "storage.delete",
1297
+ grant: "storage:kv"
1298
+ },
1299
+ "storage.keys": {
1300
+ transport: "bridge",
1301
+ method: "storage.keys",
1302
+ grant: "storage:kv"
1303
+ },
1304
+ "storage.compareAndSet": {
1305
+ transport: "bridge",
1306
+ method: "storage.compareAndSet",
1307
+ grant: "storage:kv"
1308
+ },
1309
+ // Host-direct media data-path (the host holds the node token; returns data: URLs).
1310
+ "image.generate": {
1311
+ transport: "direct",
1312
+ path: "/api/images/generate",
1313
+ grant: "media:generate"
1314
+ },
1315
+ "tts.speak": {
1316
+ transport: "direct",
1317
+ path: "/api/voice/speak",
1318
+ grant: "media:generate"
1319
+ },
1320
+ "stt.transcribe": {
1321
+ transport: "direct",
1322
+ path: "/api/voice/transcribe",
1323
+ grant: "media:transcribe"
1324
+ },
1325
+ // Broker capabilities — no rpc family yet (@requires-grant).
1326
+ "rag.retrieve": { transport: "broker", capability: "rag" },
1327
+ "rag.embed": { transport: "broker", capability: "rag" },
1328
+ "rag.rerank": { transport: "broker", capability: "rag" },
1329
+ "memory.recall": { transport: "broker", capability: "memory" },
1330
+ "memory.store": { transport: "broker", capability: "memory" },
1331
+ "realtime.broadcast": { transport: "broker", capability: "realtime" },
1332
+ "realtime.subscribe": { transport: "broker", capability: "realtime" },
1333
+ "durable.checkpoint": { transport: "broker", capability: "durable" },
1334
+ "durable.resume": { transport: "broker", capability: "durable" },
1335
+ "engines.embed": { transport: "broker", capability: "engines" }
1336
+ };
1337
+ function brokerCall(transport, cap, op, input) {
1338
+ return transport.capability(cap, { op, input });
1339
+ }
1340
+ function createPrimitives(transport) {
1341
+ return {
1342
+ background: {
1343
+ list: (input) => transport.bridge("background.list", {
1344
+ producer: input?.producer,
1345
+ running_only: input?.runningOnly
1346
+ }),
1347
+ stop: (input) => transport.bridge("background.stop", {
1348
+ process_id: input.processId
1349
+ })
1350
+ },
1351
+ storage: {
1352
+ get: (input) => transport.bridge("storage.get", input),
1353
+ set: (input) => transport.bridge("storage.set", input).then(() => void 0),
1354
+ delete: (input) => transport.bridge("storage.delete", input).then(() => void 0),
1355
+ keys: (input) => transport.bridge("storage.keys", input ?? {}).then((value) => Array.isArray(value) ? value : []),
1356
+ compareAndSet: (input) => transport.bridge("storage.compareAndSet", input)
1357
+ },
1358
+ rag: {
1359
+ retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
1360
+ embed: (input) => brokerCall(transport, "rag", "embed", input),
1361
+ rerank: (input) => brokerCall(transport, "rag", "rerank", input)
1362
+ },
1363
+ memory: {
1364
+ recall: (input) => brokerCall(transport, "memory", "recall", input),
1365
+ store: (input) => brokerCall(transport, "memory", "store", input)
1366
+ },
1367
+ realtime: {
1368
+ broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
1369
+ () => void 0
1370
+ ),
1371
+ subscribe: (input) => brokerCall(
1372
+ transport,
1373
+ "realtime",
1374
+ "subscribe",
1375
+ input
1376
+ )
1377
+ },
1378
+ durable: {
1379
+ checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
1380
+ resume: (input) => brokerCall(transport, "durable", "resume", input)
1381
+ },
1382
+ engines: {
1383
+ // Bridge family: model.complete → host.sideModel (wire keys are snake_case).
1384
+ complete: (input) => transport.bridge("model.complete", {
1385
+ prompt: input.prompt,
1386
+ system: input.system,
1387
+ model: input.model,
1388
+ model_pref_key: input.modelPrefKey,
1389
+ effort: input.effort
1390
+ }),
1391
+ embed: (input) => brokerCall(transport, "engines", "embed", input)
1392
+ },
1393
+ tts: {
1394
+ speak: (input) => transport.direct("/api/voice/speak", input)
1395
+ },
1396
+ stt: {
1397
+ transcribe: (input) => transport.direct("/api/voice/transcribe", input)
1398
+ },
1399
+ image: {
1400
+ generate: (input) => transport.direct("/api/images/generate", input)
1401
+ }
1402
+ };
1403
+ }
1404
+
1405
+ // src/runnable/skill.ts
1406
+ function defineSkill(options) {
1407
+ const { id, name, run } = options;
1408
+ return {
1409
+ id,
1410
+ name,
1411
+ kind: "skill",
1412
+ run
1413
+ };
1414
+ }
1415
+
1416
+ // src/runnable/workflow.ts
1417
+ function defineWorkflow(options) {
1418
+ const { id, name, run } = options;
1419
+ return {
1420
+ id,
1421
+ name,
1422
+ kind: "workflow",
1423
+ run
1424
+ };
1425
+ }
1426
+ // Annotate the CommonJS export names for ESM import in node:
1427
+ 0 && (module.exports = {
1428
+ PRIMITIVE_BINDINGS,
1429
+ appToolId,
1430
+ createPrimitives,
1431
+ defineAction,
1432
+ defineAgent,
1433
+ defineApp,
1434
+ defineSkill,
1435
+ defineTool,
1436
+ defineWorkflow,
1437
+ httpPrimitiveTransport
1438
+ });