@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
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Runnable — the single contract unifying Agent, Workflow, Tool, and Skill
3
- * in the Ryu SDK.
2
+ * Runnable — the execution contract unifying Agent, Workflow, Tool, and Skill
3
+ * in the Ryu SDK. `defineAction` is the governed business-operation facade over
4
+ * the Tool kind.
4
5
  *
5
6
  * Design rules (from the M8 spike doc packages/sdk/README.md):
6
7
  * - An agent may invoke a workflow as a named tool.
@@ -12,6 +13,14 @@
12
13
  * import from `@ryuhq/sdk/runnable` as a single entry point.
13
14
  */
14
15
 
16
+ export type {
17
+ ActionAnnotations,
18
+ ActionEffect,
19
+ ActionManifestOptions,
20
+ ActionOptions,
21
+ ActionRunnable,
22
+ } from "./action.ts";
23
+ export { defineAction } from "./action.ts";
15
24
  export type {
16
25
  AgentCard,
17
26
  AgentManifestOptions,
@@ -26,6 +35,8 @@ export { defineAgent } from "./agent.ts";
26
35
  export type { AppToolSpec, DefineAppOptions } from "./app.ts";
27
36
  export { appToolId, defineApp } from "./app.ts";
28
37
  export type {
38
+ BackgroundClient,
39
+ BackgroundProcess,
29
40
  DurableClient,
30
41
  EnginesClient,
31
42
  HttpPrimitiveTransportOptions,
@@ -40,6 +51,7 @@ export type {
40
51
  RealtimeClient,
41
52
  RealtimeSubscription,
42
53
  RyuPrimitives,
54
+ StorageClient,
43
55
  SttClient,
44
56
  TtsClient,
45
57
  } from "./primitives.ts";
@@ -55,7 +67,12 @@ export type {
55
67
  } from "./runnable-types.ts";
56
68
  export type { SkillOptions } from "./skill.ts";
57
69
  export { defineSkill } from "./skill.ts";
58
- export type { JsonSchemaProperty, ToolOptions, ToolSchema } from "./tool.ts";
70
+ export type {
71
+ InlineToolManifestOptions,
72
+ JsonSchemaProperty,
73
+ ToolOptions,
74
+ ToolSchema,
75
+ } from "./tool.ts";
59
76
  export { defineTool } from "./tool.ts";
60
77
  export type { WorkflowOptions, WorkflowStep } from "./workflow.ts";
61
78
  export { defineWorkflow } from "./workflow.ts";
@@ -158,6 +158,35 @@ describe("createPrimitives — transport routing mirrors rpc.ts", () => {
158
158
  expect(ragCall.body).toMatchObject({ op: "retrieve" });
159
159
  });
160
160
 
161
+ it("storage primitives use the grant-gated app-owned KV bridge", async () => {
162
+ const { transport, calls } = recordingTransport();
163
+ const p = createPrimitives(transport);
164
+ await p.storage.get({ namespace: "tickets", key: "t-1" });
165
+ await p.storage.set({ namespace: "tickets", key: "t-1", value: "open" });
166
+ await p.storage.delete({ namespace: "tickets", key: "t-1" });
167
+ await p.storage.keys({ namespace: "tickets" });
168
+ await p.storage.compareAndSet({
169
+ namespace: "tickets",
170
+ key: "t-1",
171
+ expected: null,
172
+ value: "open",
173
+ });
174
+
175
+ expect(calls.map((c) => c.target)).toEqual([
176
+ "storage.get",
177
+ "storage.set",
178
+ "storage.delete",
179
+ "storage.keys",
180
+ "storage.compareAndSet",
181
+ ]);
182
+ expect(calls.every((c) => c.op === "bridge")).toBe(true);
183
+ expect(calls[1]?.body).toEqual({
184
+ namespace: "tickets",
185
+ key: "t-1",
186
+ value: "open",
187
+ });
188
+ });
189
+
161
190
  it("every declared primitive has a binding entry (no silent drift)", () => {
162
191
  const expected = [
163
192
  "engines.complete",
@@ -174,6 +203,11 @@ describe("createPrimitives — transport routing mirrors rpc.ts", () => {
174
203
  "realtime.subscribe",
175
204
  "durable.checkpoint",
176
205
  "durable.resume",
206
+ "storage.get",
207
+ "storage.set",
208
+ "storage.delete",
209
+ "storage.keys",
210
+ "storage.compareAndSet",
177
211
  ];
178
212
  for (const key of expected) {
179
213
  expect(PRIMITIVE_BINDINGS[key]).toBeDefined();
@@ -316,6 +316,41 @@ export const PRIMITIVE_BINDINGS: Record<string, PrimitiveBinding> = {
316
316
  method: "model.complete",
317
317
  grant: "hook:side-model",
318
318
  },
319
+ "background.list": {
320
+ transport: "bridge",
321
+ method: "background.list",
322
+ grant: "background:control",
323
+ },
324
+ "background.stop": {
325
+ transport: "bridge",
326
+ method: "background.stop",
327
+ grant: "background:control",
328
+ },
329
+ "storage.get": {
330
+ transport: "bridge",
331
+ method: "storage.get",
332
+ grant: "storage:kv",
333
+ },
334
+ "storage.set": {
335
+ transport: "bridge",
336
+ method: "storage.set",
337
+ grant: "storage:kv",
338
+ },
339
+ "storage.delete": {
340
+ transport: "bridge",
341
+ method: "storage.delete",
342
+ grant: "storage:kv",
343
+ },
344
+ "storage.keys": {
345
+ transport: "bridge",
346
+ method: "storage.keys",
347
+ grant: "storage:kv",
348
+ },
349
+ "storage.compareAndSet": {
350
+ transport: "bridge",
351
+ method: "storage.compareAndSet",
352
+ grant: "storage:kv",
353
+ },
319
354
  // Host-direct media data-path (the host holds the node token; returns data: URLs).
320
355
  "image.generate": {
321
356
  transport: "direct",
@@ -468,6 +503,56 @@ export interface EnginesClient {
468
503
  }): Promise<number[][]>;
469
504
  }
470
505
 
506
+ /** One process visible through the shared Core background registry. */
507
+ export interface BackgroundProcess {
508
+ command: string;
509
+ cwd: string;
510
+ description?: string | null;
511
+ elapsed_ms: number;
512
+ exit_code?: number | null;
513
+ exit_signal?: string | null;
514
+ kind: string;
515
+ label?: string | null;
516
+ pid?: number | null;
517
+ process_id: string;
518
+ producer: string;
519
+ running: boolean;
520
+ shell_id?: string | null;
521
+ started_at: number;
522
+ }
523
+
524
+ /** Background-process registry and cooperative stop requests. */
525
+ export interface BackgroundClient {
526
+ list(input?: {
527
+ producer?: string;
528
+ runningOnly?: boolean;
529
+ }): Promise<BackgroundProcess[]>;
530
+ stop(input: { processId: string }): Promise<{
531
+ ok: boolean;
532
+ requested: boolean;
533
+ process_id: string;
534
+ }>;
535
+ }
536
+
537
+ /** Durable app-owned key/value state (`storage:kv` via the host bridge). */
538
+ export interface StorageClient {
539
+ /** Atomically replace a value when it still equals `expected`. */
540
+ compareAndSet(input: {
541
+ expected: string | null;
542
+ key: string;
543
+ namespace?: string;
544
+ value: string | null;
545
+ }): Promise<boolean>;
546
+ /** Delete one value. */
547
+ delete(input: { key: string; namespace?: string }): Promise<void>;
548
+ /** Read one value; missing keys return `null`. */
549
+ get(input: { key: string; namespace?: string }): Promise<string | null>;
550
+ /** List keys in an app-owned namespace. */
551
+ keys(input?: { namespace?: string }): Promise<string[]>;
552
+ /** Write one string value. JSON can be encoded by the app when needed. */
553
+ set(input: { key: string; namespace?: string; value: string }): Promise<void>;
554
+ }
555
+
471
556
  /** TTS primitive — speech synthesis (`crates/ryu-tts`). */
472
557
  export interface TtsClient {
473
558
  /**
@@ -513,12 +598,14 @@ export interface ImageClient {
513
598
  * Core node) — the same "what runs vs what is allowed" split as `ctx.gateway`.
514
599
  */
515
600
  export interface RyuPrimitives {
601
+ background: BackgroundClient;
516
602
  durable: DurableClient;
517
603
  engines: EnginesClient;
518
604
  image: ImageClient;
519
605
  memory: MemoryClient;
520
606
  rag: RagClient;
521
607
  realtime: RealtimeClient;
608
+ storage: StorageClient;
522
609
  stt: SttClient;
523
610
  tts: TtsClient;
524
611
  }
@@ -542,6 +629,35 @@ function brokerCall(
542
629
  */
543
630
  export function createPrimitives(transport: PrimitiveTransport): RyuPrimitives {
544
631
  return {
632
+ background: {
633
+ list: (input) =>
634
+ transport.bridge("background.list", {
635
+ producer: input?.producer,
636
+ running_only: input?.runningOnly,
637
+ }) as Promise<BackgroundProcess[]>,
638
+ stop: (input) =>
639
+ transport.bridge("background.stop", {
640
+ process_id: input.processId,
641
+ }) as Promise<{
642
+ ok: boolean;
643
+ requested: boolean;
644
+ process_id: string;
645
+ }>,
646
+ },
647
+ storage: {
648
+ get: (input) =>
649
+ transport.bridge("storage.get", input) as Promise<string | null>,
650
+ set: (input) =>
651
+ transport.bridge("storage.set", input).then(() => undefined),
652
+ delete: (input) =>
653
+ transport.bridge("storage.delete", input).then(() => undefined),
654
+ keys: (input) =>
655
+ transport
656
+ .bridge("storage.keys", input ?? {})
657
+ .then((value) => (Array.isArray(value) ? (value as string[]) : [])),
658
+ compareAndSet: (input) =>
659
+ transport.bridge("storage.compareAndSet", input) as Promise<boolean>,
660
+ },
545
661
  rag: {
546
662
  retrieve: (input) =>
547
663
  brokerCall(transport, "rag", "retrieve", input) as Promise<RagChunk[]>,
@@ -14,6 +14,7 @@ import type {
14
14
  MemoryClient,
15
15
  RagClient,
16
16
  RealtimeClient,
17
+ StorageClient,
17
18
  SttClient,
18
19
  TtsClient,
19
20
  } from "./primitives.ts";
@@ -81,6 +82,8 @@ export interface RunnableContext {
81
82
  sessionId?: string;
82
83
  /** Signal to abort a long-running run. */
83
84
  signal?: AbortSignal;
85
+ /** Durable app-owned KV state, gated by the manifest's `storage:kv` grant. */
86
+ storage?: StorageClient;
84
87
  /** STT primitive: transcribe (`crates/ryu-stt`). */
85
88
  stt?: SttClient;
86
89
  /** TTS primitive: speak (`crates/ryu-tts`). */
@@ -55,12 +55,16 @@ export interface ToolSchema {
55
55
  required?: string[];
56
56
  /** Type is always "object" for a tool's top-level input schema. */
57
57
  type: "object";
58
+ /** Preserve additional JSON Schema keywords for Core/MCP consumers. */
59
+ [key: string]: unknown;
58
60
  }
59
61
 
60
62
  // ── Options ───────────────────────────────────────────────────────────────────
61
63
 
62
64
  /** Options accepted by `defineTool`. */
63
65
  export interface ToolOptions<TInput extends Record<string, unknown>, TOutput> {
66
+ /** Optional description surfaced to models and tool discovery. */
67
+ description?: string;
64
68
  /** Stable unique identifier (e.g. "tool-web-search"). */
65
69
  id: string;
66
70
  /** Human-readable display name. */
@@ -171,6 +175,8 @@ export interface ToolRunnable<
171
175
  * the sandbox form is the second parameter aliased to `host`.
172
176
  */
173
177
  readonly code: string;
178
+ /** Optional description surfaced to models and tool discovery. */
179
+ readonly description?: string;
174
180
  readonly kind: "tool";
175
181
  /** JSON Schema for this tool's input — compatible with Core's ToolInfo.schema. */
176
182
  readonly schema: ToolSchema;
@@ -211,7 +217,7 @@ export function defineTool<
211
217
  TInput extends Record<string, unknown> = Record<string, unknown>,
212
218
  TOutput = unknown,
213
219
  >(options: ToolOptions<TInput, TOutput>): ToolRunnable<TInput, TOutput> {
214
- const { id, name, schema, run } = options;
220
+ const { description, id, name, schema, run } = options;
215
221
 
216
222
  // Serialize the run body for Core's `inline_deno` backend — the same approach
217
223
  // `defineTurnHook` uses: the sandbox wraps this in an async IIFE where `input`
@@ -223,6 +229,7 @@ export function defineTool<
223
229
  name,
224
230
  kind: "tool",
225
231
  schema,
232
+ ...(description === undefined ? {} : { description }),
226
233
  code,
227
234
  run(input: TInput, ctx: RunnableContext): Promise<TOutput> {
228
235
  validateInput(input, schema, id);
@@ -231,19 +238,33 @@ export function defineTool<
231
238
  } satisfies ToolRunnable<TInput, TOutput>;
232
239
  }
233
240
 
241
+ /** Optional metadata lowered alongside a tool's executable backend. */
242
+ export interface InlineToolManifestOptions {
243
+ /** Marks the entry as a semantic Ryu Action. */
244
+ action?: boolean;
245
+ /** MCP-style effect annotations, for example `readOnlyHint`. */
246
+ annotations?: Record<string, boolean | undefined>;
247
+ /** Description shown in Core's unified tool catalog. */
248
+ description?: string;
249
+ /** Force the human approval gate before the tool runs. */
250
+ needsApproval?: boolean;
251
+ /** JSON Schema for the structured result. */
252
+ outputSchema?: Record<string, unknown>;
253
+ }
254
+
234
255
  /**
235
256
  * Convert a {@link ToolRunnable} into a `manifest.json` `kind:"tool"` runnable that
236
257
  * ships its `run` body as Core's `inline_deno` backend. The emitted config
237
258
  * mirrors Core's `ToolConfig` (`apps/core/src/plugin_manifest/schema.rs`):
238
259
  * `{ slug, backend:"inline_deno", code, description?, input_schema }`. Core
239
- * registers it as `app__<slug>` — discoverable via `/api/tools/search` and
260
+ * registers it as `app.<slug>` — discoverable via `/api/tools/search` and
240
261
  * executed in the grant-gated sandbox.
241
262
  *
242
263
  * The plugin must declare the `tool:execute` grant (see `definePlugin`).
243
264
  */
244
265
  export function inlineToolRunnable(
245
- tool: ToolRunnable,
246
- options?: { description?: string }
266
+ tool: Pick<ToolRunnable, "code" | "description" | "id" | "name" | "schema">,
267
+ options?: InlineToolManifestOptions
247
268
  ): RunnableMeta {
248
269
  return {
249
270
  id: tool.id,
@@ -254,7 +275,17 @@ export function inlineToolRunnable(
254
275
  backend: "inline_deno",
255
276
  code: tool.code,
256
277
  input_schema: tool.schema,
257
- ...(options?.description ? { description: options.description } : {}),
278
+ ...(options?.description === undefined
279
+ ? tool.description === undefined
280
+ ? {}
281
+ : { description: tool.description }
282
+ : { description: options.description }),
283
+ ...(options?.outputSchema ? { output_schema: options.outputSchema } : {}),
284
+ ...(options?.annotations ? { annotations: options.annotations } : {}),
285
+ ...(options?.action === undefined ? {} : { action: options.action }),
286
+ ...(options?.needsApproval === undefined
287
+ ? {}
288
+ : { needs_approval: options.needsApproval }),
258
289
  },
259
290
  };
260
291
  }
@@ -18,6 +18,7 @@ import type {
18
18
  HookEventContribution,
19
19
  PluginManifest,
20
20
  RunnableMeta,
21
+ SlashCommandContribution,
21
22
  Surface,
22
23
  TurnHookContribution,
23
24
  } from "../manifest.ts";
@@ -144,8 +145,8 @@ export interface DefinePluginOptions {
144
145
  requires?: DefineAppRequires;
145
146
  /** Declarative settings tabs (model pickers, fields), passed verbatim. */
146
147
  settingsTabs?: Record<string, unknown>[];
147
- /** Declarative slash commands, passed verbatim. */
148
- slashCommands?: Record<string, unknown>[];
148
+ /** Declarative slash commands, including sequential argument choices. */
149
+ slashCommands?: SlashCommandContribution[];
149
150
  /**
150
151
  * Host surfaces this plugin runs on. **Omitted/empty = every surface** (the
151
152
  * backward-compatible default); it never means "hidden".
@@ -154,7 +155,7 @@ export interface DefinePluginOptions {
154
155
  /**
155
156
  * Inline tools the plugin ships — each a {@link ToolRunnable} from `defineTool`
156
157
  * whose `run` body is bundled as Core's `inline_deno` backend (registered as
157
- * `app__<tool.id>`). Shipping any tool auto-adds the `tool:execute` grant.
158
+ * `app.<tool.id>`). Shipping any tool auto-adds the `tool:execute` grant.
158
159
  */
159
160
  tools?: ToolRunnable[];
160
161
  /** Turn hooks the plugin contributes. */
@@ -173,6 +174,7 @@ export function definePlugin(options: DefinePluginOptions): PluginManifest {
173
174
  turn_hooks: options.turnHooks ?? [],
174
175
  hook_events: options.hookEvents ?? [],
175
176
  composer_controls: options.composerControls ?? [],
177
+ chat_features: [],
176
178
  settings_tabs: options.settingsTabs ?? [],
177
179
  slash_commands: options.slashCommands ?? [],
178
180
  lsp_servers: options.lspServers ?? {},
@@ -184,9 +186,12 @@ export function definePlugin(options: DefinePluginOptions): PluginManifest {
184
186
  sidebar_sections: [],
185
187
  sidebar_buttons: [],
186
188
  dock_panels: [],
189
+ live_activities: [],
187
190
  data_categories: [],
188
191
  pi_extensions: [],
189
192
  output_styles: [],
193
+ message_actions: [],
194
+ selection_actions: [],
190
195
  };
191
196
  // Ship each inline tool as a `kind:"tool"` runnable (Core's `inline_deno`
192
197
  // backend). Shipping tools requires the `tool:execute` grant; add it once.
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ SlashCommandContributionSchema,
4
+ app,
5
+ defineApp,
6
+ definePlugin,
7
+ } from "./index.ts";
8
+
9
+ const slashCommand = {
10
+ args: [
11
+ {
12
+ custom: { label: "Use another environment" },
13
+ name: "environment",
14
+ options: [
15
+ { label: "Staging", value: "staging" },
16
+ { label: "Production", value: "production" },
17
+ ],
18
+ },
19
+ ],
20
+ command: "/deploy",
21
+ description: "Deploy the current project",
22
+ };
23
+
24
+ describe("slash command contributions", () => {
25
+ test("validate sequential options and custom choices", () => {
26
+ expect(SlashCommandContributionSchema.parse(slashCommand)).toEqual(
27
+ slashCommand
28
+ );
29
+ });
30
+
31
+ test("defineApp publishes slash commands", () => {
32
+ const manifest = defineApp({
33
+ id: "com.example.deploy",
34
+ title: "Deploy",
35
+ version: "1.0.0",
36
+ slug: "deploy",
37
+ uiEntry: "src/deploy.tsx",
38
+ tools: [{ name: "render", description: "Render deploy status" }],
39
+ slashCommands: [slashCommand],
40
+ });
41
+
42
+ expect(manifest.contributes?.slash_commands).toEqual([slashCommand]);
43
+ });
44
+
45
+ test("definePlugin publishes slash commands", () => {
46
+ const manifest = definePlugin({
47
+ id: "com.example.deploy-plugin",
48
+ name: "Deploy",
49
+ version: "1.0.0",
50
+ slashCommands: [slashCommand],
51
+ });
52
+
53
+ expect(manifest.contributes?.slash_commands).toEqual([slashCommand]);
54
+ });
55
+
56
+ test("AppBuilder publishes slash commands", () => {
57
+ const manifest = app()
58
+ .id("com.example.deploy-builder")
59
+ .title("Deploy")
60
+ .version("1.0.0")
61
+ .slug("deploy-builder")
62
+ .uiEntry("src/deploy.tsx")
63
+ .tool({ name: "render", description: "Render deploy status" })
64
+ .slashCommand(slashCommand)
65
+ .build();
66
+
67
+ expect(manifest.contributes?.slash_commands).toEqual([slashCommand]);
68
+ });
69
+ });