@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
@@ -7,6 +7,7 @@
7
7
  import { describe, expect, test } from "bun:test";
8
8
  import {
9
9
  CompanionSurfaceSchema,
10
+ ChatWidgetTemplateSchema,
10
11
  labelImpersonatesSystemChrome,
11
12
  PluginManifestSchema,
12
13
  RunnableMetaSchema,
@@ -22,6 +23,34 @@ function baseManifest(overrides: Record<string, unknown> = {}) {
22
23
  };
23
24
  }
24
25
 
26
+ test("chat widget templates keep host rendering data-only and forward compatible", () => {
27
+ const available = ChatWidgetTemplateSchema.safeParse({
28
+ id: "meetings.chat",
29
+ title: "Meetings widget",
30
+ triggers: ["meetings widget"],
31
+ examples: ["Show my meetings"],
32
+ backing: { view_id: "surface:meetings" },
33
+ display_mode: "inline",
34
+ safe_action_ids: ["refresh"],
35
+ }).success;
36
+ const comingSoon = ChatWidgetTemplateSchema.safeParse({
37
+ id: "future.chat",
38
+ title: "Future widget",
39
+ backing: {},
40
+ display_mode: "compact-future-mode",
41
+ availability: "coming-soon",
42
+ }).success;
43
+ const unsafe = ChatWidgetTemplateSchema.safeParse({
44
+ id: "future/chat",
45
+ title: "Unsafe",
46
+ backing: { tool_id: "future.render" },
47
+ display_mode: "inline",
48
+ }).success;
49
+ expect(available).toBe(true);
50
+ expect(comingSoon).toBe(true);
51
+ expect(unsafe).toBe(false);
52
+ });
53
+
25
54
  // ── semver regex ──────────────────────────────────────────────────────────────
26
55
 
27
56
  describe("PluginManifest version (semver regex)", () => {
@@ -171,7 +200,7 @@ describe("RunnableMetaSchema", () => {
171
200
  id: "x",
172
201
  name: "X",
173
202
  kind: "tool",
174
- config: { widget: true, slug: "x__render" },
203
+ config: { widget: true, slug: "x.render" },
175
204
  });
176
205
  expect(parsed.success).toBe(true);
177
206
  if (!parsed.success) {
@@ -17,7 +17,8 @@
17
17
  *
18
18
  * The set is read from Core's tree so a new shipped plugin is covered the moment it
19
19
  * lands, without touching this file. It spans BOTH homes: the packaged manifests
20
- * live in `apps-store/<x>/manifest.json` and `plugins-store/<x>/manifest.json` (Core
20
+ * live in `apps-store/<x>/manifest.json` and
21
+ * `plugins-store/{plugins,lsp,external_plugins}/<x>/manifest.json` (Core
21
22
  * `include_str!`s them from there), and only the ~13 Core-only ones remain under
22
23
  * `apps/core/src/plugin_manifest/fixtures/`. Reading just the fixtures dir would
23
24
  * still pass — on 13 files instead of 71 — so both roots are walked deliberately.
@@ -39,7 +40,12 @@ import {
39
40
  const REPO_ROOT = join(import.meta.dir, "../../..");
40
41
  const FIXTURES_DIR = join(REPO_ROOT, "apps/core/src/plugin_manifest/fixtures");
41
42
  /** The package roots whose `manifest.json` Core compiles in directly. */
42
- const PACKAGE_ROOTS = ["apps-store", "plugins-store"];
43
+ const PACKAGE_ROOTS = [
44
+ "apps-store",
45
+ "plugins-store/plugins",
46
+ "plugins-store/lsp",
47
+ "plugins-store/external_plugins",
48
+ ];
43
49
 
44
50
  interface RawManifest {
45
51
  companion?: { label?: unknown };
@@ -127,18 +133,28 @@ const SDK_KNOWN_KINDS = new Set([
127
133
  const VALID_HOOK_PHASES = new Set([
128
134
  "post_assistant_turn",
129
135
  "pre_user_turn",
136
+ "pre_model_select",
137
+ "action",
130
138
  "session_start",
131
139
  "stop",
132
140
  "pre_tool_use",
133
141
  "post_tool_use",
142
+ "tool_result",
134
143
  "subagent_stop",
135
144
  "session_end",
136
145
  "notification",
146
+ "context",
147
+ "message_end",
148
+ "session_before_compact",
149
+ "session_compact",
150
+ "model_select",
151
+ "session_tree",
137
152
  // Kernel lifecycle phases fired by Core's own subsystems (the workflow
138
153
  // executor), not by any app.
139
154
  "workflow_run_started",
140
155
  "workflow_run_finished",
141
156
  "workflow_run_failed",
157
+ "delegation_lifecycle",
142
158
  ]);
143
159
 
144
160
  /** An **app event** — a phase some app declares in `contributes.hook_events`,
@@ -303,15 +319,15 @@ describe("glob_match oracle (ported from Core)", () => {
303
319
  expect(globMatch("bash", "bashx")).toBe(false);
304
320
  });
305
321
  it("trailing star is a prefix match", () => {
306
- expect(globMatch("bash*", "bash__run")).toBe(true);
322
+ expect(globMatch("bash*", "bash.run")).toBe(true);
307
323
  expect(globMatch("bash*", "sh")).toBe(false);
308
324
  });
309
325
  it("leading star is a suffix match", () => {
310
- expect(globMatch("*write", "fs__write")).toBe(true);
326
+ expect(globMatch("*write", "fs.write")).toBe(true);
311
327
  expect(globMatch("*write", "writer")).toBe(false);
312
328
  });
313
329
  it("double star is a substring match", () => {
314
- expect(globMatch("*edit*", "editor__do_edit")).toBe(true);
330
+ expect(globMatch("*edit*", "editor.do_edit")).toBe(true);
315
331
  expect(globMatch("*edit*", "read_only")).toBe(false);
316
332
  });
317
333
  it("an interior star degrades to an exact-literal match (Core has no compile step)", () => {
@@ -409,7 +425,7 @@ describe("PluginManifestSchema preserves turn_hook.match", () => {
409
425
  // `tool-firewall` is a packaged plugin, so its manifest lives in its
410
426
  // package directory — there is no fixture copy any more.
411
427
  readFileSync(
412
- join(REPO_ROOT, "plugins-store/tool-firewall/manifest.json"),
428
+ join(REPO_ROOT, "plugins-store/plugins/tool-firewall/manifest.json"),
413
429
  "utf8"
414
430
  )
415
431
  );
@@ -26,6 +26,7 @@ import { join } from "node:path";
26
26
  import { agent, app, PluginBuilder, skill, tool, workflow } from "./builder.ts";
27
27
  import { PluginManifestSchema } from "./manifest.ts";
28
28
  import { defineApp } from "./runnable/app.ts";
29
+ import { defineTool } from "./runnable/tool.ts";
29
30
 
30
31
  // ── builder unit tests ────────────────────────────────────────────────────────
31
32
 
@@ -302,7 +303,7 @@ describe("defineApp", () => {
302
303
  const widgets = manifest.contributes?.widgets ?? [];
303
304
 
304
305
  expect(widgets).toHaveLength(1);
305
- expect(widgets[0]?.tool_id).toBe("checklist__render");
306
+ expect(widgets[0]?.tool_id).toBe("checklist.render");
306
307
  expect(widgets[0]?.uri).toBe("ui://widget/checklist.html");
307
308
  expect(widgets[0]?.ui_entry).toBe("src/checklist.tsx");
308
309
  expect(widgets[0]?.mime).toBe("text/html+skybridge");
@@ -314,9 +315,9 @@ describe("defineApp", () => {
314
315
  expect(manifest.runnables).toHaveLength(2);
315
316
  expect(manifest.runnables.every((r) => r.kind === "tool")).toBe(true);
316
317
 
317
- const render = manifest.runnables.find((r) => r.id === "checklist__render");
318
+ const render = manifest.runnables.find((r) => r.id === "checklist.render");
318
319
  expect(render?.config).toMatchObject({
319
- slug: "checklist__render",
320
+ slug: "checklist.render",
320
321
  // The manifest is the only channel for a packed app: description +
321
322
  // input_schema must survive so Core can rebuild a driveable tool.
322
323
  description: "Render a checklist",
@@ -332,9 +333,9 @@ describe("defineApp", () => {
332
333
  invoked: "Ready",
333
334
  });
334
335
 
335
- const toggle = manifest.runnables.find((r) => r.id === "checklist__toggle");
336
+ const toggle = manifest.runnables.find((r) => r.id === "checklist.toggle");
336
337
  expect(toggle?.config).toMatchObject({
337
- slug: "checklist__toggle",
338
+ slug: "checklist.toggle",
338
339
  widget: false,
339
340
  widget_accessible: true,
340
341
  });
@@ -350,19 +351,62 @@ describe("defineApp", () => {
350
351
  uiEntry: "src/chart.tsx",
351
352
  tools: [{ name: "render", description: "Render a chart" }],
352
353
  });
353
- const render = manifest.runnables.find((r) => r.id === "chart__render");
354
+ const render = manifest.runnables.find((r) => r.id === "chart.render");
354
355
  expect(render?.config).toMatchObject({
355
356
  widget: true,
356
357
  widget_accessible: false,
357
358
  });
358
359
  // `server` override qualifies the tool id and the widget binding.
359
- expect(manifest.contributes?.widgets[0]?.tool_id).toBe("chart__render");
360
+ expect(manifest.contributes?.widgets[0]?.tool_id).toBe("chart.render");
360
361
  // The widget uri still derives from the slug, not the server.
361
362
  expect(manifest.contributes?.widgets[0]?.uri).toBe(
362
363
  "ui://widget/chart-studio.html"
363
364
  );
364
365
  });
365
366
 
367
+ it("embeds an executable ToolRunnable in the widget tool", () => {
368
+ const renderTool = defineTool({
369
+ id: "support.render",
370
+ name: "Support answer",
371
+ schema: {
372
+ type: "object",
373
+ properties: { message: { type: "string" } },
374
+ required: ["message"],
375
+ },
376
+ run: async (input) => ({
377
+ content: [{ type: "text", text: input.message }],
378
+ structuredContent: { answer: input.message },
379
+ }),
380
+ });
381
+ const manifest = defineApp({
382
+ id: "com.example.support",
383
+ title: "Support",
384
+ version: "1.0.0",
385
+ slug: "support",
386
+ uiEntry: "src/widget.html",
387
+ tools: [
388
+ {
389
+ name: "render",
390
+ description: "Answer a support question",
391
+ inputSchema: renderTool.schema as unknown as Record<string, unknown>,
392
+ runnable: renderTool,
393
+ },
394
+ ],
395
+ });
396
+ const render = manifest.runnables[0];
397
+
398
+ expect(render?.config).toMatchObject({
399
+ backend: "inline_deno",
400
+ code: renderTool.code,
401
+ input_schema: renderTool.schema,
402
+ widget: true,
403
+ });
404
+ expect(manifest.permission_grants).toEqual([
405
+ "widget:render",
406
+ "tool:execute",
407
+ ]);
408
+ });
409
+
366
410
  it("round-trips through PluginManifestSchema without stripping widgets", () => {
367
411
  // The load-bearing check: `contributes.widgets` is only preserved because it
368
412
  // was added to `ContributesSchema`. A JSON round-trip proves the field
@@ -377,12 +421,12 @@ describe("defineApp", () => {
377
421
  }
378
422
  expect(parsed.data.contributes?.widgets).toHaveLength(1);
379
423
  expect(parsed.data.contributes?.widgets[0]?.tool_id).toBe(
380
- "checklist__render"
424
+ "checklist.render"
381
425
  );
382
426
  // description + input_schema survive the strict parse (the only channel for
383
427
  // a packed app — no `generated.rs` on the Core side).
384
428
  const render = parsed.data.runnables.find(
385
- (r) => r.id === "checklist__render"
429
+ (r) => r.id === "checklist.render"
386
430
  );
387
431
  expect(render?.config?.description).toBe("Render a checklist");
388
432
  expect(render?.config?.input_schema).toBeDefined();
@@ -423,7 +467,7 @@ describe("AppBuilder", () => {
423
467
  expect(manifest.id).toBe("com.example.checklist");
424
468
  expect(manifest.runnables).toHaveLength(2);
425
469
  expect(manifest.contributes?.widgets).toHaveLength(1);
426
- expect(manifest.contributes?.widgets[0]?.tool_id).toBe("checklist__render");
470
+ expect(manifest.contributes?.widgets[0]?.tool_id).toBe("checklist.render");
427
471
  // The author's own grant, plus the `widget:render` the builder adds because
428
472
  // this app synthesises a widget — without it Core silently degrades the
429
473
  // widget to plain text.
@@ -774,3 +818,114 @@ describe("contributes.lsp_servers", () => {
774
818
  expect(parsed.data.contributes?.lsp_servers).toEqual({});
775
819
  });
776
820
  });
821
+
822
+ describe("contributes.message_actions", () => {
823
+ it("preserves renderer-specific action args through the pack-path parse", () => {
824
+ const action = {
825
+ args: {
826
+ dispatch: "reactions.toggle",
827
+ renderer: "reaction-picker",
828
+ },
829
+ capability: "reactions.toggle",
830
+ id: "reactions.picker",
831
+ kind: "menu",
832
+ label: "Add reaction",
833
+ plugin: "@ryu/reactions",
834
+ target: "user",
835
+ };
836
+ const parsed = PluginManifestSchema.safeParse({
837
+ id: "com.example.reactions",
838
+ name: "Reactions",
839
+ version: "1.0.0",
840
+ runnables: [],
841
+ contributes: { message_actions: [action] },
842
+ });
843
+
844
+ expect(parsed.success).toBe(true);
845
+ if (!parsed.success) {
846
+ return;
847
+ }
848
+ expect(parsed.data.contributes?.message_actions).toEqual([action]);
849
+ });
850
+ });
851
+
852
+ describe("contributes.selection_actions", () => {
853
+ it("preserves host-owned selection dispatch args through the pack-path parse", () => {
854
+ const action = {
855
+ args: { dispatch: "side-chat.selection", intent: "explain" },
856
+ id: "side-chats.explain-selection",
857
+ kind: "button",
858
+ label: "Explain",
859
+ order: 110,
860
+ };
861
+ const parsed = PluginManifestSchema.safeParse({
862
+ id: "com.example.side-chats",
863
+ name: "Side Chats",
864
+ version: "1.0.0",
865
+ runnables: [],
866
+ contributes: { selection_actions: [action] },
867
+ });
868
+
869
+ expect(parsed.success).toBe(true);
870
+ if (!parsed.success) {
871
+ return;
872
+ }
873
+ expect(parsed.data.contributes?.selection_actions).toEqual([action]);
874
+ });
875
+ });
876
+
877
+ describe("mcp_servers OAuth", () => {
878
+ const manifest = {
879
+ id: "com.example.mail",
880
+ mcp_servers: {
881
+ mail: {
882
+ auth: { client_id: "public-client", type: "oauth" },
883
+ type: "streamable-http",
884
+ url: "https://mcp.example.com/mcp",
885
+ },
886
+ },
887
+ name: "Mail",
888
+ permission_grants: ["mcp:server", "identity.read"],
889
+ runnables: [],
890
+ version: "1.0.0",
891
+ };
892
+
893
+ it("preserves the public OAuth declaration through the pack-path parse", () => {
894
+ const parsed = PluginManifestSchema.parse(manifest);
895
+ expect(parsed.mcp_servers?.mail?.auth).toEqual({
896
+ client_id: "public-client",
897
+ type: "oauth",
898
+ });
899
+ });
900
+
901
+ it("rejects secret auth fields and non-loopback plaintext URLs", () => {
902
+ expect(
903
+ PluginManifestSchema.safeParse({
904
+ ...manifest,
905
+ mcp_servers: {
906
+ mail: {
907
+ ...manifest.mcp_servers.mail,
908
+ auth: { client_secret: "nope", type: "oauth" },
909
+ },
910
+ },
911
+ }).success
912
+ ).toBe(false);
913
+ expect(
914
+ PluginManifestSchema.safeParse({
915
+ ...manifest,
916
+ mcp_servers: {
917
+ mail: { auth: { type: "oauth" }, url: "http://mcp.example.com" },
918
+ },
919
+ }).success
920
+ ).toBe(false);
921
+ });
922
+
923
+ it("requires both MCP and identity grants", () => {
924
+ expect(
925
+ PluginManifestSchema.safeParse({
926
+ ...manifest,
927
+ permission_grants: ["mcp:server"],
928
+ }).success
929
+ ).toBe(false);
930
+ });
931
+ });