@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/cli.cjs ADDED
@@ -0,0 +1,858 @@
1
+ #!/usr/bin/env bun
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_node_crypto = require("crypto");
28
+ var import_node_fs = require("fs");
29
+ var import_node_path = require("path");
30
+
31
+ // src/cli/dev.ts
32
+ var import_node_readline = require("readline");
33
+
34
+ // src/model/client.ts
35
+ var native = __toESM(require("@ryuhq/sdk-native"), 1);
36
+ var ModelClient2 = class {
37
+ model;
38
+ baseUrl;
39
+ native;
40
+ constructor(model, options = {}) {
41
+ this.native = new native.ModelClient(
42
+ model,
43
+ options.baseUrl ?? null,
44
+ options.token ?? null
45
+ );
46
+ this.model = model;
47
+ this.baseUrl = options.baseUrl ?? native.resolveGatewayUrl();
48
+ }
49
+ /** Send a non-streaming chat completion request to the gateway. */
50
+ async chat(messages) {
51
+ const res = await this.native.chat(messages);
52
+ const usage = res.promptTokens === void 0 && res.completionTokens === void 0 && res.totalTokens === void 0 ? void 0 : {
53
+ promptTokens: res.promptTokens ?? 0,
54
+ completionTokens: res.completionTokens ?? 0,
55
+ totalTokens: res.totalTokens ?? 0
56
+ };
57
+ return {
58
+ content: res.content,
59
+ finishReason: res.finishReason ?? null,
60
+ usage
61
+ };
62
+ }
63
+ /** Send a streaming chat completion request, yielding deltas as they arrive. */
64
+ async *stream(messages) {
65
+ const queue = [];
66
+ let done = false;
67
+ let failure = null;
68
+ let wake = null;
69
+ const notify = () => {
70
+ if (wake) {
71
+ const w = wake;
72
+ wake = null;
73
+ w();
74
+ }
75
+ };
76
+ this.native.stream(messages, (err, delta) => {
77
+ if (err) {
78
+ failure = err;
79
+ done = true;
80
+ } else if (delta === null || delta === void 0) {
81
+ done = true;
82
+ } else {
83
+ queue.push({
84
+ content: delta.content ?? null,
85
+ finishReason: delta.finishReason ?? null
86
+ });
87
+ }
88
+ notify();
89
+ });
90
+ while (true) {
91
+ while (queue.length > 0) {
92
+ const next = queue.shift();
93
+ if (next) {
94
+ yield next;
95
+ }
96
+ }
97
+ if (failure) {
98
+ throw failure;
99
+ }
100
+ if (done) {
101
+ return;
102
+ }
103
+ await new Promise((resolve2) => {
104
+ wake = resolve2;
105
+ });
106
+ }
107
+ }
108
+ };
109
+
110
+ // src/model/gateway.ts
111
+ var native2 = __toESM(require("@ryuhq/sdk-native"), 1);
112
+ function resolveGatewayUrl3() {
113
+ return native2.resolveGatewayUrl();
114
+ }
115
+
116
+ // src/cli/dev.ts
117
+ var RE_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/;
118
+ async function probeGateway(baseUrl) {
119
+ try {
120
+ await fetch(`${baseUrl}/health`, {
121
+ method: "HEAD",
122
+ signal: AbortSignal.timeout(3e3)
123
+ });
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ async function loadRunnable(entryPath) {
130
+ const abs = entryPath.startsWith("/") || RE_ABSOLUTE_PATH.test(entryPath) ? entryPath : `${process.cwd()}/${entryPath}`;
131
+ const mod = await import(abs);
132
+ const candidate = mod.default ?? mod.runnable;
133
+ if (!candidate || typeof candidate.run !== "function") {
134
+ throw new Error(
135
+ `[ryu dev] Module at "${entryPath}" must export a Runnable as "default" or "runnable". A Runnable has a "run(messages, model)" generator method.`
136
+ );
137
+ }
138
+ return candidate;
139
+ }
140
+ var ANSI = {
141
+ reset: "\x1B[0m",
142
+ bold: "\x1B[1m",
143
+ cyan: "\x1B[36m",
144
+ yellow: "\x1B[33m",
145
+ green: "\x1B[32m",
146
+ red: "\x1B[31m",
147
+ dim: "\x1B[2m"
148
+ };
149
+ function printBanner(runnableName, gatewayUrl) {
150
+ process.stdout.write(
151
+ [
152
+ "",
153
+ `${ANSI.bold}${ANSI.cyan}ryu dev${ANSI.reset} \u2014 local Runnable playground`,
154
+ `${ANSI.dim}runnable : ${runnableName}${ANSI.reset}`,
155
+ `${ANSI.dim}gateway : ${gatewayUrl}${ANSI.reset}`,
156
+ `${ANSI.dim}type "/quit" or Ctrl+C to exit${ANSI.reset}`,
157
+ ""
158
+ ].join("\n")
159
+ );
160
+ }
161
+ function printPrompt() {
162
+ process.stdout.write(`${ANSI.bold}> ${ANSI.reset}`);
163
+ }
164
+ function printAssistantStart() {
165
+ process.stdout.write(`
166
+ ${ANSI.green}assistant:${ANSI.reset} `);
167
+ }
168
+ function printAssistantEnd() {
169
+ process.stdout.write("\n");
170
+ }
171
+ function printToolCall(event) {
172
+ process.stdout.write(
173
+ `
174
+ ${ANSI.yellow}tool-call${ANSI.reset} [${event.id}] ${event.title} (${event.kind})`
175
+ );
176
+ if (event.input !== null && event.input !== void 0) {
177
+ process.stdout.write(
178
+ ` ${ANSI.dim}${JSON.stringify(event.input)}${ANSI.reset}`
179
+ );
180
+ }
181
+ process.stdout.write("\n");
182
+ }
183
+ function statusAnsiColor(status) {
184
+ if (status === "completed") {
185
+ return ANSI.green;
186
+ }
187
+ if (status === "failed") {
188
+ return ANSI.red;
189
+ }
190
+ return ANSI.dim;
191
+ }
192
+ function printToolResult(event) {
193
+ const statusColor = statusAnsiColor(event.status);
194
+ process.stdout.write(
195
+ `${ANSI.dim}tool-result${ANSI.reset} [${event.id}] ${statusColor}${event.status}${ANSI.reset}`
196
+ );
197
+ if (event.output !== null && event.output !== void 0) {
198
+ process.stdout.write(
199
+ ` ${ANSI.dim}${JSON.stringify(event.output)}${ANSI.reset}`
200
+ );
201
+ }
202
+ process.stdout.write("\n");
203
+ }
204
+ function printError(message) {
205
+ process.stderr.write(`
206
+ ${ANSI.red}error:${ANSI.reset} ${message}
207
+ `);
208
+ }
209
+ async function runDevLoop(runnable, model) {
210
+ const history = [];
211
+ const rl = (0, import_node_readline.createInterface)({
212
+ input: process.stdin,
213
+ output: process.stdout,
214
+ terminal: false
215
+ });
216
+ const linePromise = () => new Promise((resolve2) => {
217
+ rl.once("line", resolve2);
218
+ rl.once("close", () => resolve2(null));
219
+ });
220
+ printPrompt();
221
+ while (true) {
222
+ const line = await linePromise();
223
+ if (line === null) {
224
+ process.stdout.write("\n");
225
+ break;
226
+ }
227
+ const trimmed = line.trim();
228
+ if (trimmed === "") {
229
+ printPrompt();
230
+ continue;
231
+ }
232
+ if (trimmed === "/quit" || trimmed === "/exit") {
233
+ process.stdout.write("bye\n");
234
+ break;
235
+ }
236
+ history.push({ role: "user", content: trimmed });
237
+ let assistantReply = "";
238
+ printAssistantStart();
239
+ const gen = runnable.run([...history], model);
240
+ for await (const event of gen) {
241
+ switch (event.type) {
242
+ case "text": {
243
+ process.stdout.write(event.content);
244
+ assistantReply += event.content;
245
+ break;
246
+ }
247
+ case "tool_call": {
248
+ printToolCall(event);
249
+ break;
250
+ }
251
+ case "tool_result": {
252
+ printToolResult(event);
253
+ break;
254
+ }
255
+ case "error": {
256
+ printAssistantEnd();
257
+ printError(event.message);
258
+ history.pop();
259
+ assistantReply = "";
260
+ break;
261
+ }
262
+ default:
263
+ break;
264
+ }
265
+ }
266
+ printAssistantEnd();
267
+ if (assistantReply) {
268
+ history.push({ role: "assistant", content: assistantReply });
269
+ }
270
+ printPrompt();
271
+ }
272
+ rl.close();
273
+ }
274
+ async function commandDev(entryPath) {
275
+ const gatewayUrl = resolveGatewayUrl3();
276
+ process.stdout.write(`checking gateway at ${gatewayUrl} ...
277
+ `);
278
+ const reachable = await probeGateway(gatewayUrl);
279
+ if (!reachable) {
280
+ process.stderr.write(
281
+ [
282
+ "",
283
+ `${ANSI.red}error:${ANSI.reset} gateway not reachable at ${gatewayUrl}`,
284
+ "",
285
+ "The ryu dev playground requires a running Ryu gateway.",
286
+ "Start the gateway with: ryu gateway start",
287
+ "Or set RYU_GATEWAY_URL to point at a remote gateway.",
288
+ "",
289
+ "No provider fallback is attempted. Fix the gateway connection and retry.",
290
+ ""
291
+ ].join("\n")
292
+ );
293
+ process.exit(1);
294
+ }
295
+ let runnable;
296
+ try {
297
+ runnable = await loadRunnable(entryPath);
298
+ } catch (err) {
299
+ process.stderr.write(`error: ${String(err)}
300
+ `);
301
+ process.exit(1);
302
+ }
303
+ const model = new ModelClient2("default", { baseUrl: gatewayUrl });
304
+ printBanner(runnable.name, gatewayUrl);
305
+ await runDevLoop(runnable, model);
306
+ }
307
+
308
+ // src/manifest.ts
309
+ var import_node_module = require("module");
310
+ var import_zod = require("zod");
311
+ var RunnableKindSchema = import_zod.z.enum([
312
+ "agent",
313
+ "workflow",
314
+ "tool",
315
+ "skill",
316
+ // A companion surface (in-desktop panel). Added so a packable plugin can
317
+ // declare a companion runnable that flows through Core's existing Companion
318
+ // handler → app_contrib → `GET /api/plugins/contributions` → the desktop
319
+ // `/plugin/<id>` route. Its `config.ui_entry` (see `CompanionRunnableConfigSchema`)
320
+ // is what `ryu pack` bundles into `ui_code`.
321
+ "companion"
322
+ ]);
323
+ var RunnableMetaSchema = import_zod.z.object({
324
+ /** Stable unique identifier (e.g. `"agent-researcher"`). */
325
+ id: import_zod.z.string().min(1),
326
+ /** Human-readable display name. */
327
+ name: import_zod.z.string().min(1),
328
+ /** Which kind of runnable this entry describes. */
329
+ kind: RunnableKindSchema,
330
+ /**
331
+ * Optional per-kind config blob. Mirrors Core's `RunnableEntry.config`
332
+ * (`Option<serde_json::Value>`) so a manifest authored here round-trips
333
+ * through Core-strict validation. Left opaque (a record) at this authoring
334
+ * layer; the per-kind shape is enforced by Core's `validate_runnable`.
335
+ *
336
+ * For a `companion` runnable, `config.ui_entry` names the plugin's UI entry
337
+ * module (relative to the manifest dir). `ryu pack` bundles that entry into
338
+ * the emitted `ui_code`; Core's `CompanionConfig.ui_entry` is the lockstep
339
+ * field so a packed companion validates.
340
+ */
341
+ config: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
342
+ });
343
+ function labelImpersonatesSystemChrome(label) {
344
+ const lower = label.toLowerCase();
345
+ return lower.includes("ryu") || lower.includes("system");
346
+ }
347
+ var CompanionSurfaceSchema = import_zod.z.object({
348
+ /** Display label for the companion panel tab or tooltip. Anti-impersonation:
349
+ * may not pose as first-party Ryu/system chrome (see
350
+ * {@link labelImpersonatesSystemChrome}). */
351
+ label: import_zod.z.string().min(1).refine((value) => !labelImpersonatesSystemChrome(value), {
352
+ message: "companion label must not impersonate system chrome (must not contain 'ryu' or 'system')"
353
+ }),
354
+ /** Icon identifier resolved by the desktop shell. */
355
+ icon: import_zod.z.string().optional(),
356
+ /** Keyboard shortcut string (e.g. `"ctrl+shift+r"`). */
357
+ shortcut: import_zod.z.string().optional()
358
+ });
359
+ var TurnHookContributionSchema = import_zod.z.object({
360
+ /** Stable id for this hook (unique within the plugin). */
361
+ id: import_zod.z.string().min(1),
362
+ /** Turn boundary this fires on. Today only `"post_assistant_turn"`. */
363
+ on: import_zod.z.string().min(1).default("post_assistant_turn"),
364
+ /** The JS hook body executed in the sandbox (returns a directive). */
365
+ code: import_zod.z.string().min(1)
366
+ });
367
+ var DEFAULT_WIDGET_MIME = "text/html+skybridge";
368
+ var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
369
+ var WidgetContributionSchema = import_zod.z.object({
370
+ /** The fully-qualified tool id whose result renders this widget. */
371
+ tool_id: import_zod.z.string().min(1),
372
+ /** `ui://widget/<slug>.html` — the widget resource uri. */
373
+ uri: import_zod.z.string().min(1),
374
+ /** Source entry (e.g. `src/apps/checklist/index.tsx`) for `ryu pack`. */
375
+ ui_entry: import_zod.z.string().optional(),
376
+ /** Widget MIME dialect (default `text/html+skybridge`). */
377
+ mime: import_zod.z.string().default(DEFAULT_WIDGET_MIME),
378
+ /** Default display mode (`inline` | `fullscreen` | `pip`). */
379
+ default_display_mode: import_zod.z.string().default(DEFAULT_WIDGET_DISPLAY_MODE)
380
+ });
381
+ var ToolAppConfigSchema = import_zod.z.object({
382
+ /** MCP tool slug this runnable wraps — the fully-qualified `<server>__<name>` id. */
383
+ slug: import_zod.z.string().min(1),
384
+ /** The tool description the model reads when choosing it. Carried here because a
385
+ * packed app's manifest is the only channel (there is no `generated.rs`); Core's
386
+ * app-tool synthesis reads it back onto the `RegistryTool`. */
387
+ description: import_zod.z.string(),
388
+ /** JSON Schema for the tool's arguments (used for validation + the LLM tool
389
+ * surface). Snake_case to match `widget_accessible`. Absent = no arguments. */
390
+ input_schema: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
391
+ /** True when calling this tool renders the app's widget (carries the template). */
392
+ widget: import_zod.z.boolean().default(false),
393
+ /** True when a mounted widget may `callTool` this tool (a companion), or when a
394
+ * render tool's widget may call any companion the app declares. */
395
+ widget_accessible: import_zod.z.boolean().default(false),
396
+ /** Optional status label shown while the render tool runs. */
397
+ invoking: import_zod.z.string().optional(),
398
+ /** Optional status label shown when the render tool finishes. */
399
+ invoked: import_zod.z.string().optional()
400
+ });
401
+ var ContributesSchema = import_zod.z.object({
402
+ turn_hooks: import_zod.z.array(TurnHookContributionSchema).default([]),
403
+ composer_controls: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
404
+ settings_tabs: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
405
+ slash_commands: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.unknown())).default([]),
406
+ /** App widgets (Ryu Apps). Each binds a render tool id to its
407
+ * `ui://widget/<slug>.html` template. Mirrors the Rust-side
408
+ * `Contributes.widgets` field, without which the CLI's zod parse would strip
409
+ * every widget an app authored here declares. */
410
+ widgets: import_zod.z.array(WidgetContributionSchema).default([])
411
+ });
412
+ var SetupStepSchema = import_zod.z.object({
413
+ /** Card heading (e.g. the companion app name). */
414
+ title: import_zod.z.string().optional(),
415
+ /** Instruction body shown under the title. */
416
+ description: import_zod.z.string().optional(),
417
+ /** Label for the optional action button. */
418
+ actionLabel: import_zod.z.string().optional(),
419
+ /** URL the action button opens (validated server-side on publish). */
420
+ actionUrl: import_zod.z.string().optional()
421
+ });
422
+ var AppDependencySchema = import_zod.z.object({
423
+ /** The `id` of the plugin this one depends on. */
424
+ id: import_zod.z.string().min(1, "dependency id is required"),
425
+ /** Optional MINIMUM version the dependency must satisfy (`"1.2.0"` = `">=1.2.0"`). */
426
+ min_version: import_zod.z.string().min(1).optional()
427
+ });
428
+ var CapabilityReqSchema = import_zod.z.object({
429
+ /** Capability name (e.g. `"rag"`, `"memory"`, `"tts"`). Matched against a
430
+ * provider's `provides[].capability`. */
431
+ capability: import_zod.z.string().min(1, "capability name is required"),
432
+ /** Optional MINIMUM capability version the bound provider must satisfy
433
+ * (`"1.2.0"` = `">=1.2.0"`). Absent = any version. */
434
+ min_version: import_zod.z.string().min(1).optional()
435
+ });
436
+ var RequiresSchema = import_zod.z.object({
437
+ /** Other plugins that must be installed + enabled before this one enables. */
438
+ apps: import_zod.z.array(AppDependencySchema).default([]),
439
+ /**
440
+ * Abstract capability edges the broker resolves to a bound provider at
441
+ * enable time. Mirrors `Requires::capabilities` in
442
+ * `crates/ryu-kernel-contracts` — an `apps` edge names a specific plugin; a
443
+ * `capabilities` edge names a capability and lets the binding registry choose
444
+ * the provider. Each is lowered to an app-id graph edge once bound, so the
445
+ * enable/disable/cycle machinery is shared. Empty for the common case.
446
+ */
447
+ capabilities: import_zod.z.array(CapabilityReqSchema).default([]),
448
+ /**
449
+ * Permission grants implied by the dependencies. Declaration only — the
450
+ * Gateway remains the sole authority on what a grant *allows*, and Core's
451
+ * dependency graph resolves `apps` only.
452
+ */
453
+ grants: import_zod.z.array(import_zod.z.string()).default([])
454
+ });
455
+ var SurfaceSchema = import_zod.z.enum([
456
+ /** The Ryu Gateway. */
457
+ "gateway",
458
+ /** A headless Core node (no UI). */
459
+ "core",
460
+ /** The Tauri desktop app. */
461
+ "desktop",
462
+ /** The Electron dynamic-island companion. */
463
+ "island",
464
+ /** The Expo/React-Native mobile app. */
465
+ "mobile",
466
+ /** The browser extension. */
467
+ "extension",
468
+ /** The Next.js web app. */
469
+ "web",
470
+ /** The terminal client. */
471
+ "cli"
472
+ ]);
473
+ var PluginManifestSchema = import_zod.z.object({
474
+ /** Reverse-domain unique identifier (e.g. `"com.example.my-plugin"`). */
475
+ id: import_zod.z.string().min(1, "id is required"),
476
+ /** Human-readable display name shown in the plugin store / launcher. */
477
+ name: import_zod.z.string().min(1, "name is required"),
478
+ /**
479
+ * Semver version string (e.g. `"1.0.0"`). Core's loader rejects any manifest
480
+ * whose version is not valid semver; the regex here enforces the same rule at
481
+ * SDK-build time.
482
+ */
483
+ version: import_zod.z.string().regex(
484
+ /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/,
485
+ "version must be a valid semver string (e.g. 1.0.0)"
486
+ ),
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
+ /**
505
+ * Optional Companion surface (an in-desktop overlay or sidebar panel).
506
+ * Absent when the plugin has no Companion surface.
507
+ */
508
+ companion: CompanionSurfaceSchema.optional(),
509
+ /**
510
+ * VS-Code-style activation events (`"*"`, `"onStartup"`, `"onChat"`,
511
+ * `"onCommand:<id>"`). Empty = eager. Turn-hook plugins are driven by their
512
+ * enabled flag, so `["*"]` is the usual value.
513
+ */
514
+ activation_events: import_zod.z.array(import_zod.z.string()).default([]),
515
+ /**
516
+ * Contribution points: server-side turn hooks + declarative UI widgets.
517
+ * Absent for a plugin that contributes nothing here.
518
+ */
519
+ contributes: ContributesSchema.optional(),
520
+ /**
521
+ * **Plugin-to-plugin dependencies** — the other plugins this one needs. Core
522
+ * resolves them into a topological enable order (dependencies enable first;
523
+ * disabling one is refused while an enabled dependent needs it).
524
+ *
525
+ * Absent = **no dependencies**, the backward-compatible default. Kept
526
+ * `.optional()` (never defaulted) so a manifest that declares none serialises
527
+ * with no `requires` key at all, exactly like Core's
528
+ * `#[serde(skip_serializing_if = "Option::is_none")]`.
529
+ */
530
+ requires: RequiresSchema.optional(),
531
+ /**
532
+ * Host surfaces this plugin runs on. **Empty or absent = runs on EVERY
533
+ * surface** — the backward-compatible default, which must never be read as
534
+ * "runs nowhere". Core filters only when the list is explicitly non-empty, and
535
+ * only at the read boundary (`GET /api/plugins`, keyed on `x-ryu-surface`), so
536
+ * an unsupported-target plugin stays installable and inspectable.
537
+ */
538
+ targets: import_zod.z.array(SurfaceSchema).default([]),
539
+ /**
540
+ * Optional per-item AFFILIATE terms: the commission paid to a referrer when a
541
+ * referred user buys this (paid) item. `value` is basis points for `percent`
542
+ * (2000 = 20%) or minor units (cents) for `flat`. Absent (or `enabled:false`)
543
+ * falls back to the seller org owner's default affiliate terms. This is the
544
+ * authoring surface for the marketplace publish body's `affiliate` field (the
545
+ * server re-validates it); it only takes effect on a paid item.
546
+ */
547
+ affiliate: import_zod.z.object({
548
+ enabled: import_zod.z.boolean().default(false),
549
+ rule: import_zod.z.object({
550
+ type: import_zod.z.enum(["percent", "flat"]),
551
+ value: import_zod.z.number().nonnegative(),
552
+ recurring: import_zod.z.boolean().default(false),
553
+ durationMonths: import_zod.z.number().int().positive().nullish(),
554
+ fundedBy: import_zod.z.enum(["platform", "seller"]).default("platform")
555
+ }).optional()
556
+ }).optional(),
557
+ // ── Rich listing metadata (Phase 1.5) ──────────────────────────────────────
558
+ // Optional store-listing fields a plugin author declares so the marketplace
559
+ // detail dialog renders a richer App-Store-style preview. Field names align
560
+ // with the Claude `.claude-plugin/marketplace.json` plugin-entry standard where
561
+ // one exists (`author`, `homepage`, `keywords`, `category`, `license`); the
562
+ // rest are Ryu extensions. `ryu publish` forwards these FLAT into the publish
563
+ // body (not inside the signed manifest blob) so the control plane stores them.
564
+ // All optional + additive: a manifest omitting them still validates.
565
+ /** Longer plain/markdown description shown in the detail dialog. */
566
+ description: import_zod.z.string().optional(),
567
+ /** Short one-line pitch shown under the name (Ryu extension). */
568
+ tagline: import_zod.z.string().optional(),
569
+ /**
570
+ * Publisher identity. A bare string OR a Claude-style object; `ryu publish`
571
+ * resolves it to the display `developer` (`author.name` when an object).
572
+ */
573
+ author: import_zod.z.union([
574
+ import_zod.z.string(),
575
+ import_zod.z.object({
576
+ name: import_zod.z.string(),
577
+ email: import_zod.z.string().optional(),
578
+ url: import_zod.z.string().optional()
579
+ })
580
+ ]).optional(),
581
+ /** Project/marketing homepage — maps to the listing `website` (Claude field). */
582
+ homepage: import_zod.z.string().optional(),
583
+ /** Free-text search keywords (Claude field). */
584
+ keywords: import_zod.z.array(import_zod.z.string()).optional(),
585
+ /** Taxonomy category beyond the runnable kinds (Claude field). */
586
+ category: import_zod.z.string().optional(),
587
+ /** SPDX-ish license identifier (Claude field). */
588
+ license: import_zod.z.string().optional(),
589
+ /** Square logo/icon URL for the listing card + detail header. */
590
+ iconUrl: import_zod.z.string().optional(),
591
+ /** Ordered App-Store-style screenshot gallery URLs (Ryu extension). */
592
+ screenshots: import_zod.z.array(import_zod.z.string()).optional(),
593
+ /** Privacy policy URL surfaced on detail (Ryu extension). */
594
+ privacyPolicyUrl: import_zod.z.string().optional(),
595
+ /** Terms-of-service URL surfaced on detail (Ryu extension). */
596
+ termsOfServiceUrl: import_zod.z.string().optional(),
597
+ /**
598
+ * Human-readable capability strings (Ryu extension). When omitted the control
599
+ * plane derives a default from `permission_grants`, so declaring this is only
600
+ * needed to override the derived labels.
601
+ */
602
+ capabilities: import_zod.z.array(import_zod.z.string()).optional(),
603
+ /** Example prompt chips shown on detail (Ryu extension). */
604
+ examplePrompts: import_zod.z.array(import_zod.z.string()).optional(),
605
+ /**
606
+ * Optional companion/config card (Ryu extension): a single setup step or an
607
+ * array of steps guiding the user through post-install configuration.
608
+ */
609
+ setup: import_zod.z.union([SetupStepSchema, import_zod.z.array(SetupStepSchema)]).optional()
610
+ });
611
+
612
+ // src/cli.ts
613
+ function uiCodeSha256(code) {
614
+ return (0, import_node_crypto.createHash)("sha256").update(code, "utf8").digest("hex");
615
+ }
616
+ function printUsage() {
617
+ process.stderr.write(
618
+ [
619
+ "Ryu dev SDK",
620
+ "",
621
+ "Usage:",
622
+ " bunx ryu pack <dir> Validate and bundle a plugin.json Plugin",
623
+ " bunx ryu publish <dir> Validate and publish a plugin.json Plugin to the Ryu Marketplace",
624
+ " bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
625
+ ""
626
+ ].join("\n")
627
+ );
628
+ }
629
+ function exitError(message) {
630
+ process.stderr.write(`error: ${message}
631
+ `);
632
+ process.exit(1);
633
+ }
634
+ function loadManifest(dir) {
635
+ const manifestPath = (0, import_node_path.join)(dir, "plugin.json");
636
+ if (!(0, import_node_fs.existsSync)(manifestPath)) {
637
+ exitError(`plugin.json not found in: ${dir}`);
638
+ }
639
+ let raw;
640
+ try {
641
+ raw = (0, import_node_fs.readFileSync)(manifestPath, "utf8");
642
+ } catch (err) {
643
+ exitError(`could not read ${manifestPath}: ${String(err)}`);
644
+ }
645
+ let parsed;
646
+ try {
647
+ parsed = JSON.parse(raw);
648
+ } catch {
649
+ exitError(`plugin.json is not valid JSON: ${manifestPath}`);
650
+ }
651
+ const result = PluginManifestSchema.safeParse(parsed);
652
+ if (!result.success) {
653
+ const first = result.error.issues[0];
654
+ const field = first?.path.join(".") ?? "unknown";
655
+ const message = first?.message ?? "validation failed";
656
+ exitError(`plugin.json validation failed at '${field}': ${message}`);
657
+ }
658
+ return result.data;
659
+ }
660
+ function resolveUiEntry(manifest) {
661
+ for (const runnable of manifest.runnables) {
662
+ if (runnable.kind !== "companion") {
663
+ continue;
664
+ }
665
+ const entry = runnable.config?.ui_entry;
666
+ if (typeof entry === "string" && entry.trim().length > 0) {
667
+ return entry;
668
+ }
669
+ }
670
+ for (const widget of manifest.contributes?.widgets ?? []) {
671
+ const entry = widget.ui_entry;
672
+ if (typeof entry === "string" && entry.trim().length > 0) {
673
+ return entry;
674
+ }
675
+ }
676
+ return null;
677
+ }
678
+ function resolveUiFormat(manifest) {
679
+ for (const runnable of manifest.runnables) {
680
+ if (runnable.kind !== "companion") {
681
+ continue;
682
+ }
683
+ const fmt = runnable.config?.ui_format;
684
+ if (typeof fmt === "string" && fmt.trim().toLowerCase() === "html") {
685
+ return "html";
686
+ }
687
+ }
688
+ return "js";
689
+ }
690
+ function readUiEntryHtml(dir, uiEntry) {
691
+ const entryPath = (0, import_node_path.resolve)(dir, uiEntry);
692
+ if (!(0, import_node_fs.existsSync)(entryPath)) {
693
+ exitError(`companion ui_entry (html) not found: ${entryPath}`);
694
+ }
695
+ return (0, import_node_fs.readFileSync)(entryPath, "utf8");
696
+ }
697
+ async function bundleUiEntry(dir, uiEntry) {
698
+ const entryPath = (0, import_node_path.resolve)(dir, uiEntry);
699
+ if (!(0, import_node_fs.existsSync)(entryPath)) {
700
+ exitError(`companion ui_entry not found: ${entryPath}`);
701
+ }
702
+ const result = await Bun.build({
703
+ entrypoints: [entryPath],
704
+ target: "browser",
705
+ format: "esm",
706
+ minify: false
707
+ });
708
+ if (!result.success) {
709
+ const messages = result.logs.map((l) => String(l.message)).join("; ");
710
+ exitError(`failed to bundle ui_entry '${uiEntry}': ${messages}`);
711
+ }
712
+ const output = result.outputs[0];
713
+ if (!output) {
714
+ exitError(`bundling ui_entry '${uiEntry}' produced no output`);
715
+ }
716
+ return await output.text();
717
+ }
718
+ async function commandPack(rawDir) {
719
+ const dir = (0, import_node_path.resolve)(rawDir);
720
+ const manifest = loadManifest(dir);
721
+ const uiEntry = resolveUiEntry(manifest);
722
+ const uiCode = uiEntry ? resolveUiFormat(manifest) === "html" ? readUiEntryHtml(dir, uiEntry) : await bundleUiEntry(dir, uiEntry) : null;
723
+ const manifestWithHash = uiCode ? { ...manifest, ui_code_sha256: uiCodeSha256(uiCode) } : manifest;
724
+ const outDir = (0, import_node_path.join)(dir, "dist");
725
+ if (!(0, import_node_fs.existsSync)(outDir)) {
726
+ (0, import_node_fs.mkdirSync)(outDir, { recursive: true });
727
+ }
728
+ const outPath = (0, import_node_path.join)(outDir, "plugin.bundle.json");
729
+ const bundle = uiCode ? { ...manifestWithHash, ui_code: uiCode } : manifestWithHash;
730
+ (0, import_node_fs.writeFileSync)(outPath, JSON.stringify(bundle, null, 2), "utf8");
731
+ const codeNote = uiCode ? ` (+${uiCode.length}B ui_code)` : "";
732
+ process.stdout.write(
733
+ `packed ${manifest.id}@${manifest.version}${codeNote} \u2192 ${outPath}
734
+ `
735
+ );
736
+ }
737
+ var TRAILING_SLASHES = /\/+$/;
738
+ function publishBaseUrl() {
739
+ const raw = (process.env.RYU_MARKETPLACE_API_URL ?? "").trim();
740
+ return (raw || "http://localhost:3000").replace(TRAILING_SLASHES, "");
741
+ }
742
+ function authToken() {
743
+ const token = (process.env.RYU_AUTH_TOKEN ?? "").trim();
744
+ if (!token) {
745
+ exitError(
746
+ "publish requires an auth token: set RYU_AUTH_TOKEN to your Ryu access token"
747
+ );
748
+ }
749
+ return token;
750
+ }
751
+ var SDK_PUBLISH_KIND = "plugin";
752
+ async function commandPublish(rawDir) {
753
+ const dir = (0, import_node_path.resolve)(rawDir);
754
+ const manifest = loadManifest(dir);
755
+ const token = authToken();
756
+ const kind = SDK_PUBLISH_KIND;
757
+ const uiEntry = resolveUiEntry(manifest);
758
+ const uiCode = uiEntry ? resolveUiFormat(manifest) === "html" ? readUiEntryHtml(dir, uiEntry) : await bundleUiEntry(dir, uiEntry) : null;
759
+ const manifestWithHash = uiCode ? { ...manifest, ui_code_sha256: uiCodeSha256(uiCode) } : manifest;
760
+ const developer = typeof manifest.author === "string" ? manifest.author : manifest.author?.name;
761
+ const runnablesForDisplay = manifest.runnables.map((r) => ({
762
+ id: r.id,
763
+ kind: r.kind,
764
+ name: r.name,
765
+ enabled: true
766
+ }));
767
+ const listingMetadata = {
768
+ ...manifest.description ? { description: manifest.description } : {},
769
+ ...manifest.tagline ? { tagline: manifest.tagline } : {},
770
+ ...developer ? { developer } : {},
771
+ ...manifest.category ? { category: manifest.category } : {},
772
+ ...manifest.iconUrl ? { iconUrl: manifest.iconUrl } : {},
773
+ ...manifest.screenshots?.length ? { screenshots: manifest.screenshots } : {},
774
+ ...manifest.homepage ? { website: manifest.homepage } : {},
775
+ ...manifest.privacyPolicyUrl ? { privacyPolicyUrl: manifest.privacyPolicyUrl } : {},
776
+ ...manifest.termsOfServiceUrl ? { termsOfServiceUrl: manifest.termsOfServiceUrl } : {},
777
+ ...manifest.capabilities?.length ? { capabilities: manifest.capabilities } : {},
778
+ ...manifest.examplePrompts?.length ? { examplePrompts: manifest.examplePrompts } : {},
779
+ ...manifest.setup ? { setup: manifest.setup } : {},
780
+ ...runnablesForDisplay.length ? { runnables: runnablesForDisplay } : {}
781
+ };
782
+ const url = `${publishBaseUrl()}/api/marketplace/publish`;
783
+ const body = {
784
+ id: manifest.id,
785
+ kind,
786
+ name: manifest.name,
787
+ version: manifest.version,
788
+ manifest: manifestWithHash,
789
+ // The descriptor is the manifest itself for a plugin/skill Plugin; Core maps
790
+ // it on install. Grants are read from the manifest server-side too.
791
+ descriptor: manifestWithHash,
792
+ grants: manifest.permission_grants ?? [],
793
+ // Rich listing metadata (Phase 1.5) forwarded flat; see above.
794
+ ...listingMetadata,
795
+ // Per-item affiliate terms (optional): the commission a referrer earns when
796
+ // a referred user buys this paid item. The server re-validates the rule and
797
+ // stores it as the item's override (else the seller default applies).
798
+ ...manifest.affiliate?.enabled ? { affiliate: manifest.affiliate } : {},
799
+ // The bundled UI code rides OUTSIDE the signed manifest as payload; the
800
+ // server stores it and serves it on detail. Omitted for manifest-only.
801
+ ...uiCode ? { ui_code: uiCode } : {}
802
+ };
803
+ let resp;
804
+ try {
805
+ resp = await fetch(url, {
806
+ method: "POST",
807
+ headers: {
808
+ "content-type": "application/json",
809
+ authorization: `Bearer ${token}`
810
+ },
811
+ body: JSON.stringify(body)
812
+ });
813
+ } catch (err) {
814
+ exitError(`could not reach ${url}: ${String(err)}`);
815
+ }
816
+ const text = await resp.text();
817
+ if (!resp.ok) {
818
+ exitError(`publish failed (${resp.status}): ${text}`);
819
+ }
820
+ process.stdout.write(
821
+ `published ${manifest.id}@${manifest.version} (${kind}) \u2192 pending moderation
822
+ ${text}
823
+ `
824
+ );
825
+ }
826
+ var [, , command, ...args] = process.argv;
827
+ if (!command) {
828
+ printUsage();
829
+ process.exit(1);
830
+ }
831
+ if (command === "pack") {
832
+ const dir = args[0];
833
+ if (!dir) {
834
+ exitError("pack requires a directory argument: bunx ryu pack <dir>");
835
+ }
836
+ commandPack(dir).catch((err) => {
837
+ exitError(String(err));
838
+ });
839
+ } else if (command === "publish") {
840
+ const dir = args[0];
841
+ if (!dir) {
842
+ exitError("publish requires a directory argument: bunx ryu publish <dir>");
843
+ }
844
+ commandPublish(dir).catch((err) => {
845
+ exitError(String(err));
846
+ });
847
+ } else if (command === "dev") {
848
+ const entry = args[0];
849
+ if (!entry) {
850
+ exitError("dev requires an entry argument: bunx ryu dev <entry>");
851
+ }
852
+ commandDev(entry).catch((err) => {
853
+ exitError(String(err));
854
+ });
855
+ } else {
856
+ printUsage();
857
+ exitError(`unknown command: ${command}`);
858
+ }