@ryuhq/sdk 0.1.3 → 0.1.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.
@@ -129,7 +129,12 @@ export interface PluginManifest {
129
129
  */
130
130
  capabilities?: string[];
131
131
  /**
132
- * Free-text category (Claude `category`).
132
+ * Free-text category (Claude `category`). The Store groups its Apps and
133
+ * Plugins tabs by this string, so two listings that mean the same shelf must
134
+ * spell it the same way — see the canonical set in `docs/`-adjacent
135
+ * `STORE_CATEGORY_ORDER` (`packages/marketplace/src/catalog/categories.ts`),
136
+ * which also decides shelf ORDER. An unrecognised value still renders; it just
137
+ * sorts after the known shelves, so a new category needs no client release.
133
138
  */
134
139
  category?: string | null;
135
140
  /**
@@ -161,6 +166,20 @@ export interface PluginManifest {
161
166
  * Prompt-chip examples (contract key `examplePrompts`; Ryu extension).
162
167
  */
163
168
  examplePrompts?: string[];
169
+ /**
170
+ * Hide this listing from the Store without uninstalling or disabling it.
171
+ *
172
+ * The listing keeps working for anyone who already has it — this is a
173
+ * *catalog* control, not a lifecycle one. It exists so an app that is built
174
+ * but not ready to be discovered can ship dark: the manifest stays compiled
175
+ * in, the routes stay registered, and the card simply is not offered.
176
+ *
177
+ * Absent ⇒ visible, matching the identically-named field the published
178
+ * `marketplace.json` already carries for third-party indexes
179
+ * (`catalog_source::sources`), so both tiers spell "don't list this" the same
180
+ * way and a client that predates the field just shows everything.
181
+ */
182
+ hidden?: boolean;
164
183
  /**
165
184
  * Homepage/website URL (Claude `homepage`; emitted as `website`).
166
185
  */
@@ -171,6 +190,13 @@ export interface PluginManifest {
171
190
  * the shared `Icon` primitive. Distinct from `icon_url`: this is a GLYPH id the
172
191
  * card masks with `currentColor`, `icon_url` is a raster logo. When absent the
173
192
  * card falls back to `icon_url`, then a default glyph.
193
+ *
194
+ * One id shape is NOT a glyph: `svgl:<slug>` (or `svgl:<light>|<dark>`) names a
195
+ * brand mark on svgl.app, which the card renders as a full-colour image instead
196
+ * — masking a brand's logo to `currentColor` would flatten it to a silhouette.
197
+ * Prefer it over `icon_url` for a listing that fronts a known product (Brave,
198
+ * Firecrawl, Notion, …): it is a stable, versionless id rather than a URL that
199
+ * can rot, and svgl's own API supplies the dark-theme variant when one exists.
174
200
  */
175
201
  icon?: string | null;
176
202
  /**
@@ -204,6 +230,22 @@ export interface PluginManifest {
204
230
  * SPDX license identifier (Claude `license`).
205
231
  */
206
232
  license?: string | null;
233
+ /**
234
+ * This plugin is REQUIRED FOR CORE: the UI must never offer to disable or
235
+ * uninstall it, and the lifecycle refuses both — with no `force` escape, which
236
+ * is what separates it from the softer
237
+ * [`crate::manifest`]-external load-bearing guard.
238
+ *
239
+ * **Declaring this does not grant it.** A manifest is untrusted input, and an
240
+ * undisableable plugin is exactly what a hostile one would ask to be, so the
241
+ * enforcement set is a Core-owned constant (`plugins::builtins::
242
+ * MANDATORY_PLUGINS`) and this field is only the manifest-side declaration of
243
+ * it. A bijection test keeps the two in lockstep, and a third-party manifest
244
+ * that sets it is ignored by the lifecycle — it only ever affects how the
245
+ * listing renders. Same posture as `CORE_PLUGINS`: privilege is never
246
+ * self-asserted.
247
+ */
248
+ mandatory?: boolean;
207
249
  /**
208
250
  * Declarative **stdio MCP servers** this plugin registers into Core's MCP
209
251
  * registry on enable and deregisters on disable/uninstall. Each entry is a
@@ -368,6 +410,15 @@ export interface PluginManifest {
368
410
  * entry's `source`/`builtin` pair.
369
411
  */
370
412
  source?: string | null;
413
+ /**
414
+ * How finished this listing is: `alpha`, `beta`, `rc`, … Absent or `stable`
415
+ * means finished and renders no badge.
416
+ *
417
+ * Free-form, NOT an enum, for the same reason the marketplace-index copy of
418
+ * this field is: an unrecognised tier renders verbatim rather than being
419
+ * dropped, so publishing a `canary` needs no client release.
420
+ */
421
+ stability?: string | null;
371
422
  /**
372
423
  * Per-surface support + UI declaration — the richer successor to [`targets`].
373
424
  *
@@ -450,7 +501,8 @@ export interface CompanionSurface {
450
501
  * Most surfaces added since are **self-contained**: they carry their own payload
451
502
  * and reference no runnable at all (`widgets`, `views`, `dock_panels`,
452
503
  * `sidebar_sections`, `sidebar_buttons`, `settings_tabs`, `composer_controls`,
453
- * `slash_commands`, `turn_hooks`, `tool_filters`, `lsp_servers`).
504
+ * `slash_commands`, `turn_hooks`, `tool_filters`, `lsp_servers`,
505
+ * `message_actions`, `context_menu_items`).
454
506
  *
455
507
  * # Extending
456
508
  *
@@ -531,6 +583,18 @@ export interface Contributes {
531
583
  * older shells" instead of breaking the composer.
532
584
  */
533
585
  composer_controls?: unknown[];
586
+ /**
587
+ * Context-menu rows the plugin contributes to a shell entity menu (the
588
+ * conversation-row dropdown, a message right-click, a space row). Lets an app
589
+ * own a menu row instead of the shell hardcoding it (e.g. "Make a skill from
590
+ * this chat" is a Learning contribution, not an `AppSidebar` if). See
591
+ * [`ContextMenuContribution`]; served + tagged with the owning `plugin` id at
592
+ * `GET /api/plugins/contributions`.
593
+ *
594
+ * **Stored raw, validated at the chokepoint** — same rule as
595
+ * [`Contributes::message_actions`].
596
+ */
597
+ context_menu_items?: ContextMenuContribution[];
534
598
  /**
535
599
  * **Deletable data categories** the app owns — one "Delete all X" row in
536
600
  * Settings → Danger Zone (see [`DataCategoryContribution`]).
@@ -660,6 +724,62 @@ export interface Contributes {
660
724
  lsp_servers?: {
661
725
  [k: string]: LspServerContribution;
662
726
  };
727
+ /**
728
+ * Per-message actions the plugin contributes to the desktop message toolbar
729
+ * (thumbs, rate, transform, …). Lets an app own a control in the per-message
730
+ * toolbar instead of the shell welding the action into the closed set of
731
+ * built-in toolbar buttons. Self-contained + opaque `spec` (see
732
+ * [`MessageActionContribution`]), so a new action kind needs no Core change;
733
+ * served + tagged with the owning `plugin` id at
734
+ * `GET /api/plugins/contributions`. A renderer that does not know a `kind`
735
+ * ignores it, so an older shell degrades to "not shown" rather than breaking.
736
+ *
737
+ * **Stored raw, validated at the chokepoint** — same rule as
738
+ * [`Contributes::settings_tabs`]: the desktop forwards the original bytes, so
739
+ * a shell newer than this Core build still gets every field it was shipped to
740
+ * render.
741
+ */
742
+ message_actions?: MessageActionContribution[];
743
+ /**
744
+ * **Output styles** the plugin ships — Markdown files that change *how* an agent
745
+ * answers (role, tone, default response shape) by editing the system prompt for
746
+ * the turn. See `docs/output-styles.md`.
747
+ *
748
+ * A style is NOT its own catalog kind, for exactly the argument
749
+ * [`Contributes::themes`] makes one field up: as a contribution it inherits
750
+ * install/uninstall/enable, versioning, signing, the Store detail page, reviews
751
+ * and the trust scorecard for free, and a plugin that ships a style ALONGSIDE
752
+ * other contributions (an app with a matching voice) stays expressible. A
753
+ * `CatalogKind::OutputStyle` would have been a second, weaker copy of all of
754
+ * that — and `CatalogKind::ALL` is a closed five-member enum that must stay
755
+ * that way, because every surface that switches on it exhaustively is a place a
756
+ * sixth member would have to be threaded by hand.
757
+ *
758
+ * # Safe with zero grants, unlike the other file-bearing family here
759
+ *
760
+ * The body is prose: nothing in the pipeline evaluates it, it only ever lands in
761
+ * a system prompt as text. So a style sits with themes on the safe side of the
762
+ * line — the worst a hostile one can do is make the agent tiresome — and
763
+ * pointedly NOT with [`Contributes::pi_extensions`], which is unsandboxed code
764
+ * and therefore tier-gated at the materializer.
765
+ *
766
+ * # Served on the contributions endpoint
767
+ *
768
+ * Unlike [`Contributes::pi_extensions`] and [`Contributes::lsp_servers`], which
769
+ * Core consumes at their own sites, this one IS served from
770
+ * `GET /api/plugins/contributions` — the desktop composer's style picker is a
771
+ * client-rendered surface and needs the declaration, not just its effect.
772
+ *
773
+ * Self-contained (it names no runnable), so it stays out of
774
+ * [`Contributes::referenced_ids`].
775
+ *
776
+ * ```json
777
+ * "output_styles": [
778
+ * { "id": "eli5", "file": "output-styles/eli5.md" }
779
+ * ]
780
+ * ```
781
+ */
782
+ output_styles?: OutputStyleContribution[];
663
783
  /**
664
784
  * **Pi extensions** the plugin ships — TypeScript files the managed `ryu` (Pi)
665
785
  * agent loads at process start:
@@ -742,6 +862,50 @@ export interface Contributes {
742
862
  * the resulting message. Served + rendered the same way.
743
863
  */
744
864
  slash_commands?: unknown[];
865
+ /**
866
+ * App-registered **marketplace tabs** — one section in the Store's nav bar,
867
+ * carrying the app's own installable catalog (workflow templates, meeting-notes
868
+ * templates, monitor presets, …). The Store-shaped sibling of
869
+ * [`Contributes::dock_panels`]: it lets an app own its browse-and-install
870
+ * surface instead of the shell welding the section into a closed `StoreSection`
871
+ * union. Self-contained + opaque `spec` (see [`StoreTabContribution`]).
872
+ *
873
+ * **Served OUTSIDE the enabled filter**, unlike every sibling family here: each
874
+ * entry is tagged with `plugin` plus `app_installed` / `app_enabled` and the
875
+ * renderer decides. Serving the declaration unconditionally keeps the door open
876
+ * for a surface that wants the tab as an acquisition funnel; the DATA behind it
877
+ * stays gated by the app's own route gate either way.
878
+ *
879
+ * The desktop Store deliberately renders only the tabs whose app is installed
880
+ * AND enabled. A pill present whether or not you own the app reads exactly like
881
+ * a section the shell hardcoded, and clicking it produced a "Turn on X" prompt
882
+ * where a catalog belongs. Apps are acquired from the Apps tab; the app's own
883
+ * sections appear with it.
884
+ */
885
+ store_tabs?: StoreTabContribution[];
886
+ /**
887
+ * **Colour themes** the plugin ships — the seam that makes a theme an ordinary
888
+ * marketplace item instead of a hardcoded entry in the shell's preset table.
889
+ *
890
+ * This is deliberately the VS Code / Zed shape: a theme is not its own catalog
891
+ * kind, it is a plugin that contributes one. That choice is load-bearing rather
892
+ * than cosmetic — it means a theme inherits install/uninstall/enable, versioning,
893
+ * signing, the Store detail page, reviews and the trust scorecard for free, and
894
+ * it means a plugin that ships a theme ALONGSIDE other contributions (an app with
895
+ * a matching skin) is expressible. A new `CatalogKind::Theme` would have bought a
896
+ * second, weaker copy of all of that.
897
+ *
898
+ * Each entry is a [`ThemeContribution`]: pure design tokens, no code. Themes are
899
+ * therefore the one contribution family that is safe with **zero** grants — the
900
+ * worst a hostile theme can do is look bad, because the shell only ever reads
901
+ * `tokens` into CSS custom properties and never evaluates them.
902
+ *
903
+ * Self-contained (it names no runnable), so it stays out of
904
+ * [`Contributes::referenced_ids`]. Typed rather than opaque JSON because Core
905
+ * does interpret it: the mode/token split is what lets a client ask for "the dark
906
+ * themes" without parsing every payload.
907
+ */
908
+ themes?: ThemeContribution[];
745
909
  /**
746
910
  * Tools this plugin wants **hidden** from the model's offered tool list —
747
911
  * the declarative half of a tool firewall (see [`ToolFilterContribution`]).
@@ -816,6 +980,57 @@ export interface ContributionId {
816
980
  */
817
981
  title?: string | null;
818
982
  }
983
+ /**
984
+ * One context-menu row a plugin contributes (see
985
+ * [`Contributes::context_menu_items`]).
986
+ */
987
+ export interface ContextMenuContribution {
988
+ /**
989
+ * WHICH menu. Closed-ish enum by convention, open by encoding (same call as
990
+ * [`DockPanelPlacement`]): `"conversation"` | `"message"` | `"space"` |
991
+ * `"agent"` | `"project"` | `"workflow"` | `"skill"` | `"channel"`. The shell
992
+ * owns the anchor set; an app cannot conjure a new menu, but an unknown value
993
+ * must not fail the load.
994
+ *
995
+ * An anchor names an ENTITY, not a place, so one declaration reaches every
996
+ * surface that shows it. The desktop renders these in the sidebar row's menu
997
+ * AND — for whatever entity a tab is showing — in that tab's right-click menu,
998
+ * on both the horizontal strip and the vertical tab list. `"channel"` is in the
999
+ * list because a channel is one of those tab-visible entities; it is not a
1000
+ * desktop-only extension.
1001
+ */
1002
+ anchor: string;
1003
+ args?: unknown;
1004
+ /**
1005
+ * The granted capability the shell invokes when the row is clicked, plus
1006
+ * static `args`. Never inline code, never a capability the owning plugin was
1007
+ * not granted.
1008
+ */
1009
+ capability: string;
1010
+ /**
1011
+ * Optional feedback text for the shell's toast: `{ loading, success, error }`.
1012
+ * Lets the app own its copy without owning the toast component.
1013
+ */
1014
+ feedback?: {
1015
+ [k: string]: unknown;
1016
+ };
1017
+ /**
1018
+ * Optional glyph id resolved by the shell's Icon primitive.
1019
+ */
1020
+ icon?: string | null;
1021
+ /**
1022
+ * Stable id for this row within the plugin.
1023
+ */
1024
+ id: string;
1025
+ /**
1026
+ * Row label.
1027
+ */
1028
+ label: string;
1029
+ /**
1030
+ * Sort position among contributed rows (ascending).
1031
+ */
1032
+ order?: number | null;
1033
+ }
819
1034
  /**
820
1035
  * One **deletable data category** an app owns (see [`Contributes::data_categories`]).
821
1036
  *
@@ -1108,6 +1323,107 @@ export interface LspServerContribution {
1108
1323
  */
1109
1324
  workspaceFolder?: string | null;
1110
1325
  }
1326
+ /**
1327
+ * One per-message toolbar action a plugin contributes (see
1328
+ * [`Contributes::message_actions`]).
1329
+ *
1330
+ * The `kind` discriminant is deliberately NOT an enum (same reasoning as
1331
+ * [`ViewContribution::view`]): a member an older shell has never heard of must
1332
+ * reach a newer shell intact rather than being rejected at load. Renderers ignore
1333
+ * a `kind` they do not know, so a new kind degrades to "not shown" instead of
1334
+ * breaking the message toolbar.
1335
+ */
1336
+ export interface MessageActionContribution {
1337
+ args?: unknown;
1338
+ /**
1339
+ * The granted capability the shell invokes when the action fires, plus static
1340
+ * `args`. Never inline code, never a capability the owning plugin was not
1341
+ * granted — identical to the `action` composer control's dispatch rule.
1342
+ */
1343
+ capability: string;
1344
+ /**
1345
+ * Optional glyph id resolved by the shell's Icon primitive (Iconify/Hugeicons).
1346
+ */
1347
+ icon?: string | null;
1348
+ /**
1349
+ * Stable id for this action within the plugin (the shell's element key and
1350
+ * dispatch tag, namespaced as `plugin:<pluginId>:<id>`).
1351
+ */
1352
+ id: string;
1353
+ /**
1354
+ * Render mode: `"button"` (fire-and-forget) | `"toggle-group"` (mutually
1355
+ * exclusive states, what thumbs is) | `"menu"`. Open string.
1356
+ */
1357
+ kind: string;
1358
+ /**
1359
+ * Accessible label (tooltip / aria-label) for the action button.
1360
+ */
1361
+ label: string;
1362
+ /**
1363
+ * Sort position among contributed actions (ascending).
1364
+ */
1365
+ order?: number | null;
1366
+ /**
1367
+ * Optional `ViewSource` the shell polls to hydrate current state (what lights
1368
+ * the thumb on reload). Same `/api/`-path guard as views.
1369
+ */
1370
+ state_source?: {
1371
+ [k: string]: unknown;
1372
+ };
1373
+ /**
1374
+ * For `kind: "toggle-group"`: the states, each `{ value, label, icon?,
1375
+ * active_icon? }`. Opaque to Core; the renderer owns the shape.
1376
+ */
1377
+ states?: {
1378
+ [k: string]: unknown;
1379
+ };
1380
+ /**
1381
+ * Which messages the action attaches to: `"assistant"` | `"user"` | `"any"`.
1382
+ * Open string — an unknown role is ignored, not rejected.
1383
+ */
1384
+ target: string;
1385
+ }
1386
+ /**
1387
+ * One **output style** a plugin ships (a [`Contributes::output_styles`] row).
1388
+ *
1389
+ * Carries the style's *body* in one of two forms and NOTHING else — no `name`, no
1390
+ * `description`, no `keep-coding-instructions`. Every one of those lives in the
1391
+ * file's own YAML frontmatter, which [`PluginManifest::hydrate_output_style_files`]
1392
+ * explains: mirroring them up here would create a second place a style's metadata
1393
+ * can be stated, and therefore a place it can disagree with itself.
1394
+ */
1395
+ export interface OutputStyleContribution {
1396
+ /**
1397
+ * SOURCE form: path to the Markdown file, relative to the plugin root — exactly
1398
+ * `output-styles/<name>.md`. See [`validate_output_style_path`].
1399
+ *
1400
+ * Exactly one of `file` / `source` is set. Authors write `file`;
1401
+ * [`PluginManifest::hydrate_output_style_files`] turns it into `source` at parse
1402
+ * time and clears this, so a hydrated manifest is byte-indistinguishable from
1403
+ * one that was authored inline.
1404
+ */
1405
+ file?: string | null;
1406
+ /**
1407
+ * Stable id for this style within the plugin (`[a-z0-9][a-z0-9._-]*`).
1408
+ *
1409
+ * Validated with the same alphabet as a [`PiExtensionContribution::id`], and for
1410
+ * a related reason: the registry merges plugin, user, project and managed styles
1411
+ * into one id-keyed table where later entries win, and the persisted per-turn /
1412
+ * per-conversation / node-default selection is this id. A free-form id would
1413
+ * make a selection unresolvable the moment it contained something a settings key
1414
+ * or a URL path could not carry.
1415
+ */
1416
+ id: string;
1417
+ /**
1418
+ * WIRE form: the file's contents **verbatim, frontmatter included**.
1419
+ *
1420
+ * Deliberately the whole file rather than a pre-split body, so that a style
1421
+ * contributed by a plugin and a style sitting in a user's `output-styles/`
1422
+ * directory are the same bytes and go through the same single parser. See
1423
+ * [`PluginManifest::hydrate_output_style_files`].
1424
+ */
1425
+ source?: string | null;
1426
+ }
1111
1427
  /**
1112
1428
  * One **Pi extension** a plugin ships (a [`Contributes::pi_extensions`] row).
1113
1429
  *
@@ -1318,6 +1634,113 @@ export interface SidebarSectionContribution {
1318
1634
  */
1319
1635
  title: string;
1320
1636
  }
1637
+ /**
1638
+ * One app-registered **marketplace tab** — a section in the Store's nav bar whose
1639
+ * content is the app's own installable catalog. A typed envelope around an opaque
1640
+ * `spec` (the `StoreTabSpec` in `@ryu/app-host/views`: a `ViewSource` for the rows,
1641
+ * a `groupBy`/`groups` split into card sections, an `install` action, and per-item
1642
+ * actions). Core stores it verbatim and tags it with the owning `plugin` id; the
1643
+ * `spec` stays opaque so a new catalog capability is a renderer change, not a Core
1644
+ * change.
1645
+ *
1646
+ * **There is no first-party escape hatch.** This contribution used to carry a
1647
+ * `view` naming a hand-written renderer the shell kept in a plugin-id allowlist,
1648
+ * for the one tab whose detail pane the vocabulary could not express (the
1649
+ * workflow-template graph). That made the flagship example of "an app can own a
1650
+ * Store section" the single section no other app could reproduce. The graph is a
1651
+ * declarative primitive now (`spec.detail.graph`), the field is gone, and every
1652
+ * contributed tab — first-party or not — renders from the same spec.
1653
+ */
1654
+ export interface StoreTabContribution {
1655
+ /**
1656
+ * Which nav cluster the pill joins — the shell draws a divider wherever the
1657
+ * group changes. Built-in groups: `discover`, `catalog`, `community`, `manage`,
1658
+ * `account`. An unknown value gets its own cluster rather than being dropped.
1659
+ */
1660
+ group?: string | null;
1661
+ /**
1662
+ * Optional glyph id resolved by the shell's Icon primitive (Iconify/Hugeicons).
1663
+ */
1664
+ icon?: string | null;
1665
+ /**
1666
+ * Stable id for this tab within the plugin. The shell namespaces it into the
1667
+ * section key as `plugin:<pluginId>:<id>` so two apps can both ship a
1668
+ * `templates` tab.
1669
+ */
1670
+ id: string;
1671
+ /**
1672
+ * Placement hint within the group (lower = further left).
1673
+ */
1674
+ order?: number | null;
1675
+ /**
1676
+ * The opaque tab spec (source/map/groups/search/install/itemActions). Interpreted
1677
+ * by the desktop renderer, never by Core. Absent alongside an absent `view` = an
1678
+ * empty tab.
1679
+ */
1680
+ spec?: {
1681
+ [k: string]: unknown;
1682
+ };
1683
+ /**
1684
+ * One-line description shown under the title in the section header.
1685
+ */
1686
+ subtitle?: string | null;
1687
+ /**
1688
+ * Nav-pill label.
1689
+ */
1690
+ title: string;
1691
+ }
1692
+ /**
1693
+ * One colour theme a plugin contributes (`contributes.themes`).
1694
+ *
1695
+ * Shape-identical to the shell's own `ThemeVariant` (`@ryu/ui/theme/presets`), so a
1696
+ * theme installed from the marketplace and a theme that ships in the binary are the
1697
+ * same object by the time the picker renders them — there is no second rendering
1698
+ * path to keep in sync, and a plugin can never express a theme the built-ins could
1699
+ * not.
1700
+ *
1701
+ * # Why `tokens` is an untyped map
1702
+ *
1703
+ * The keys are CSS custom properties (`--background`, `--sidebar-ring`, …). Typing
1704
+ * them as a fixed struct would mean every new token added to the design system
1705
+ * silently DROPS out of third-party themes until Core is rebuilt and redeployed —
1706
+ * exactly the drift `settings_tabs` documents for its own `serde_json::Value`. The
1707
+ * values are never evaluated, only assigned to CSS variables, so an unknown key is
1708
+ * inert rather than dangerous.
1709
+ */
1710
+ export interface ThemeContribution {
1711
+ /**
1712
+ * Stable id used as the persisted preset selection. Namespace it with the
1713
+ * plugin id (e.g. `"@acme/themes:midnight"`) so two plugins cannot collide, and
1714
+ * so a selection survives the theme being renamed.
1715
+ */
1716
+ id: string;
1717
+ /**
1718
+ * Human name shown in the theme picker.
1719
+ */
1720
+ label: string;
1721
+ /**
1722
+ * Which mode slot this theme fills: `"light"` or `"dark"`. A plugin shipping a
1723
+ * pair contributes two entries, mirroring how the shell keeps an independent
1724
+ * preset per mode rather than one theme with two halves.
1725
+ */
1726
+ mode: string;
1727
+ preview: ThemePreview;
1728
+ /**
1729
+ * CSS custom property name → value (e.g. `"--background"` → `"oklch(1 0 0)"`).
1730
+ */
1731
+ tokens: {
1732
+ [k: string]: string;
1733
+ };
1734
+ }
1735
+ /**
1736
+ * The four swatch colours the picker paints before the theme is applied.
1737
+ */
1738
+ export interface ThemePreview {
1739
+ bg: string;
1740
+ primary: string;
1741
+ surface: string;
1742
+ text: string;
1743
+ }
1321
1744
  /**
1322
1745
  * One **tool filter**: a fully-qualified tool id a plugin wants withheld from the
1323
1746
  * model's offered tool list.
package/src/manifest.ts CHANGED
@@ -274,6 +274,46 @@ export type PiExtensionContribution = z.infer<
274
274
  typeof PiExtensionContributionSchema
275
275
  >;
276
276
 
277
+ // ── OutputStyleContribution ──────────────────────────────────────────────────
278
+
279
+ /**
280
+ * One output style the plugin ships — a Markdown file (YAML frontmatter + prose)
281
+ * that rewrites the system prompt's voice for a turn. Mirrors the Rust-side
282
+ * `OutputStyleContribution`.
283
+ *
284
+ * Unlike `pi_extensions` above, this one is INLINED by `ryu pack`: `file` is the
285
+ * source form and `source` is the wire form, exactly as `code_file` → `code`. That
286
+ * is why both fields exist here and only `file` exists there — a style body is
287
+ * prose nothing evaluates, so inlining it costs no auditability (the whole point of
288
+ * keeping `pi-extensions/*.ts` out of the manifest), and it is what keeps the body
289
+ * inside the Gateway-signed surface instead of relying on a directory the installed
290
+ * plugin does not carry.
291
+ *
292
+ * Typed rather than a loose record for the same reason `pi_extensions` is: three
293
+ * fields, all of them Ryu's own vocabulary. Deliberately NOT refined to
294
+ * "exactly one of `file` / `source`" — Core's `Contributes::validate_output_styles`
295
+ * is the single gate for that rule, and a second copy here is a place the two can
296
+ * disagree about a manifest that has already been hydrated once.
297
+ */
298
+ export const OutputStyleContributionSchema = z.object({
299
+ /** Stable id for this style within the plugin (`[a-z0-9][a-z0-9._-]*`). It is
300
+ * also the persisted selection key, so it must survive a settings key and a URL
301
+ * path. */
302
+ id: z.string().min(1),
303
+ /** SOURCE form: path to the Markdown file, relative to the plugin root —
304
+ * exactly `output-styles/<name>.md`. `ryu pack` replaces this with `source`. */
305
+ file: z.string().min(1).optional(),
306
+ /** WIRE form: the file's contents verbatim, frontmatter INCLUDED. The whole file
307
+ * rather than a pre-split body plus mirrored `name`/`description` keys, so a
308
+ * plugin style and a user's own `output-styles/*.md` go through one parser and
309
+ * the frontmatter stays the single source of truth for a style's metadata. */
310
+ source: z.string().optional(),
311
+ });
312
+
313
+ export type OutputStyleContribution = z.infer<
314
+ typeof OutputStyleContributionSchema
315
+ >;
316
+
277
317
  // ── WidgetContribution (Ryu Apps) ─────────────────────────────────────────────
278
318
 
279
319
  /** Default widget MIME dialect. Mirrors Core `default_widget_mime`. */
@@ -402,6 +442,14 @@ export const ContributesSchema = z.object({
402
442
  * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
403
443
  * Claude Code's to extend. */
404
444
  pi_extensions: z.array(PiExtensionContributionSchema).default([]),
445
+ /** Output styles the plugin ships — Markdown files that rewrite the system
446
+ * prompt's voice. Mirrors the Rust-side `Contributes.output_styles`; without it
447
+ * the CLI's zod parse would strip the declaration, and `ryu pack` would sign a
448
+ * bundle whose styles simply do not exist. Worse than the usual case of that
449
+ * bug: the styles' `.md` files are not carried by the bundle either, so there
450
+ * would be no residue to notice — the plugin would install clean and contribute
451
+ * nothing. */
452
+ output_styles: z.array(OutputStyleContributionSchema).default([]),
405
453
  });
406
454
 
407
455
  export type Contributes = z.infer<typeof ContributesSchema>;
@@ -228,10 +228,13 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
228
228
  // takes no `contributes` passthrough. An app that wants to declare language
229
229
  // servers writes them in a hand-authored `manifest.json`.
230
230
  lsp_servers: {},
231
- // Same reason again: a danger-zone category and a Pi extension are both
232
- // hand-authored declarations, not something derivable from runnables.
231
+ // Same reason again: a danger-zone category, a Pi extension and an output
232
+ // style are all hand-authored declarations, not something derivable from
233
+ // runnables. An output style in particular points at a Markdown file next
234
+ // to the manifest, which this builder never writes.
233
235
  data_categories: [],
234
236
  pi_extensions: [],
237
+ output_styles: [],
235
238
  widgets,
236
239
  };
237
240
 
@@ -177,15 +177,16 @@ export function definePlugin(options: DefinePluginOptions): PluginManifest {
177
177
  slash_commands: options.slashCommands ?? [],
178
178
  lsp_servers: options.lspServers ?? {},
179
179
  // A turn-hook plugin contributes no app widgets, sidebar entries, dock
180
- // panels, danger-zone categories or Pi extensions; the fields are required
181
- // on the resolved `Contributes` type (zod defaults applied), so set them
182
- // explicitly.
180
+ // panels, danger-zone categories, Pi extensions or output styles; the
181
+ // fields are required on the resolved `Contributes` type (zod defaults
182
+ // applied), so set them explicitly.
183
183
  widgets: [],
184
184
  sidebar_sections: [],
185
185
  sidebar_buttons: [],
186
186
  dock_panels: [],
187
187
  data_categories: [],
188
188
  pi_extensions: [],
189
+ output_styles: [],
189
190
  };
190
191
  // Ship each inline tool as a `kind:"tool"` runnable (Core's `inline_deno`
191
192
  // backend). Shipping tools requires the `tool:execute` grant; add it once.