@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.
- package/dist/agent-plugin.cjs +242 -0
- package/dist/agent-plugin.d.cts +159 -0
- package/dist/agent-plugin.d.ts +159 -0
- package/dist/agent-plugin.js +22 -0
- package/dist/{chunk-XTUK5I6I.js → chunk-CUY2QOFC.js} +24 -1
- package/dist/chunk-G6FLVEC4.js +210 -0
- package/dist/cli.cjs +324 -5
- package/dist/cli.js +117 -6
- package/dist/index.cjs +33 -7
- package/dist/index.js +11 -7
- package/dist/manifest.cjs +25 -1
- package/dist/manifest.d.cts +36 -1
- package/dist/manifest.d.ts +36 -1
- package/dist/manifest.js +3 -1
- package/package.json +7 -2
- package/src/agent-plugin.test.ts +222 -0
- package/src/agent-plugin.ts +424 -0
- package/src/cli.ts +199 -6
- package/src/generated/plugin-manifest.ts +425 -2
- package/src/manifest.ts +48 -0
- package/src/runnable/app.ts +5 -2
- package/src/runnable/turn-hook.ts +4 -3
|
@@ -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>;
|
package/src/runnable/app.ts
CHANGED
|
@@ -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
|
|
232
|
-
// hand-authored declarations, not something derivable from
|
|
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
|
|
181
|
-
// on the resolved `Contributes` type (zod defaults
|
|
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.
|