@ryuhq/sdk 0.0.5 → 0.1.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.
- package/dist/agent.cjs +4 -1
- package/dist/agent.d.cts +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{chunk-KPKMMGVC.js → chunk-MTUBUPIV.js} +4 -1
- package/dist/{chunk-GXHL5CO7.js → chunk-XTUK5I6I.js} +110 -2
- package/dist/cli.cjs +163 -10
- package/dist/cli.js +56 -9
- package/dist/{index-DAxq7Y0R.d.ts → index-B6SkaAjJ.d.ts} +4 -4
- package/dist/{index-CEbS1SlS.d.cts → index-BvAB5eMk.d.cts} +4 -4
- package/dist/index.cjs +167 -9
- package/dist/index.d.cts +30 -12
- package/dist/index.d.ts +30 -12
- package/dist/index.js +57 -8
- package/dist/manifest.cjs +112 -2
- package/dist/manifest.d.cts +131 -10
- package/dist/manifest.d.ts +131 -10
- package/dist/manifest.js +5 -1
- package/package.json +4 -4
- package/src/builder.ts +4 -4
- package/src/cli.ts +98 -13
- package/src/contracts-lockstep.test.ts +4 -4
- package/src/generated/plugin-manifest.ts +1284 -24
- package/src/manifest-schema.test.ts +182 -0
- package/src/manifest.fixtures.test.ts +447 -0
- package/src/manifest.test.ts +100 -16
- package/src/manifest.ts +189 -17
- package/src/plugin/ryu-plugin.ts +1 -1
- package/src/runnable/agent.ts +3 -3
- package/src/runnable/app.test.ts +67 -0
- package/src/runnable/app.ts +58 -4
- package/src/runnable/primitives.test.ts +1 -5
- package/src/runnable/primitives.ts +6 -5
- package/src/runnable/tool.ts +4 -4
- package/src/runnable/turn-hook.ts +33 -4
package/src/runnable/app.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A "Ryu App" bundles one or more tools whose results render an interactive
|
|
5
5
|
* widget inline in chat (the ChatGPT-Apps-style surface). `defineApp` assembles a
|
|
6
|
-
* complete `
|
|
6
|
+
* complete `manifest.json` `PluginManifest` from a declarative description, deriving
|
|
7
7
|
* the render-vs-companion split exactly the way Core's in-process provider does
|
|
8
8
|
* (`apps/core/src/sidecar/mcp/apps/mod.rs` `tools()`):
|
|
9
9
|
*
|
|
@@ -37,6 +37,28 @@ const DEFAULT_APP_WIDGET_MIME = "text/html+skybridge";
|
|
|
37
37
|
/** The default widget display mode (mirrors Core `default_widget_display_mode`). */
|
|
38
38
|
const DEFAULT_APP_DISPLAY_MODE = "inline";
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The grant Core requires before it will promote a tool's result into an inline
|
|
42
|
+
* chat widget (mirrors Core's `WIDGET_RENDER_GRANT`).
|
|
43
|
+
*/
|
|
44
|
+
export const WIDGET_RENDER_GRANT = "widget:render";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `grants` plus {@link WIDGET_RENDER_GRANT} when the app actually contributes a
|
|
48
|
+
* widget. Order-preserving and idempotent, so an author who already declared it
|
|
49
|
+
* gets no duplicate.
|
|
50
|
+
*/
|
|
51
|
+
function withWidgetRenderGrant(
|
|
52
|
+
grants: readonly string[],
|
|
53
|
+
widgets: readonly WidgetContribution[]
|
|
54
|
+
): string[] {
|
|
55
|
+
const out = [...grants];
|
|
56
|
+
if (widgets.length > 0 && !out.includes(WIDGET_RENDER_GRANT)) {
|
|
57
|
+
out.push(WIDGET_RENDER_GRANT);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
40
62
|
/** One tool a Ryu App declares. */
|
|
41
63
|
export interface AppToolSpec {
|
|
42
64
|
/**
|
|
@@ -123,7 +145,7 @@ export function appToolId(server: string, name: string): string {
|
|
|
123
145
|
}
|
|
124
146
|
|
|
125
147
|
/**
|
|
126
|
-
* Assemble a `
|
|
148
|
+
* Assemble a `manifest.json` manifest for a Ryu App. The result matches Core's
|
|
127
149
|
* `PluginManifest` serde shape (validated through `PluginManifestSchema`) and can
|
|
128
150
|
* be written to disk, packed with `ryu pack`, or published with `ryu publish`.
|
|
129
151
|
*
|
|
@@ -191,9 +213,25 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
191
213
|
|
|
192
214
|
const contributes: Contributes = {
|
|
193
215
|
turn_hooks: [],
|
|
216
|
+
// This builder synthesises an app from its runnables; an app that emits
|
|
217
|
+
// events declares them in a hand-authored `manifest.json`, same as
|
|
218
|
+
// `lsp_servers` below.
|
|
219
|
+
hook_events: [],
|
|
194
220
|
composer_controls: [],
|
|
195
221
|
settings_tabs: [],
|
|
196
222
|
slash_commands: [],
|
|
223
|
+
sidebar_sections: [],
|
|
224
|
+
sidebar_buttons: [],
|
|
225
|
+
dock_panels: [],
|
|
226
|
+
// Empty for the same reason as every sibling family above: this builder
|
|
227
|
+
// synthesises `widgets` from the app's own runnables and nothing else, and
|
|
228
|
+
// takes no `contributes` passthrough. An app that wants to declare language
|
|
229
|
+
// servers writes them in a hand-authored `manifest.json`.
|
|
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.
|
|
233
|
+
data_categories: [],
|
|
234
|
+
pi_extensions: [],
|
|
197
235
|
widgets,
|
|
198
236
|
};
|
|
199
237
|
|
|
@@ -202,7 +240,21 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
202
240
|
name: options.title,
|
|
203
241
|
version: options.version,
|
|
204
242
|
runnables,
|
|
205
|
-
|
|
243
|
+
// An app that synthesises widgets MUST hold `widget:render`, so this
|
|
244
|
+
// builder declares it rather than leaving the author to discover it.
|
|
245
|
+
//
|
|
246
|
+
// Core gates widget promotion on declared-AND-enabled-AND-granted, and a
|
|
247
|
+
// missing grant fails as `DeniedNoGrant` — which is an `info!` log and
|
|
248
|
+
// nothing else. The widget silently renders as plain text, with no error
|
|
249
|
+
// in the UI and nothing pointing at the manifest. Every app scaffolded
|
|
250
|
+
// through `defineApp` hit that, because the only fix was a grant string
|
|
251
|
+
// the templates never mention and the builder never added; the one
|
|
252
|
+
// working example on disk hand-writes it.
|
|
253
|
+
//
|
|
254
|
+
// Added only when there is a widget to render, and unioned rather than
|
|
255
|
+
// overwritten so an author's own `grants` list survives and re-declaring
|
|
256
|
+
// it is not an error.
|
|
257
|
+
permission_grants: withWidgetRenderGrant(options.grants ?? [], widgets),
|
|
206
258
|
activation_events: options.activationEvents ?? ["*"],
|
|
207
259
|
contributes,
|
|
208
260
|
// `targets: []` means EVERY surface, so an app that declares none is
|
|
@@ -227,7 +279,9 @@ export function defineApp(options: DefineAppOptions): PluginManifest {
|
|
|
227
279
|
const first = result.error.issues[0];
|
|
228
280
|
const field = first?.path.join(".") ?? "unknown";
|
|
229
281
|
const message = first?.message ?? "validation failed";
|
|
230
|
-
throw new Error(
|
|
282
|
+
throw new Error(
|
|
283
|
+
`manifest.json validation failed at '${field}': ${message}`
|
|
284
|
+
);
|
|
231
285
|
}
|
|
232
286
|
return result.data;
|
|
233
287
|
}
|
|
@@ -77,11 +77,7 @@ describe("createPrimitives — transport routing mirrors rpc.ts", () => {
|
|
|
77
77
|
let seen: { url: string; init: RequestInit } | undefined;
|
|
78
78
|
const fetchImpl = ((url: string, init: RequestInit) => {
|
|
79
79
|
seen = { url, init };
|
|
80
|
-
return Promise.resolve(
|
|
81
|
-
new Response(JSON.stringify({ text: " hello world " }), {
|
|
82
|
-
headers: { "content-type": "application/json" },
|
|
83
|
-
})
|
|
84
|
-
);
|
|
80
|
+
return Promise.resolve(Response.json({ text: " hello world " }));
|
|
85
81
|
}) as unknown as typeof fetch;
|
|
86
82
|
|
|
87
83
|
const transport = httpPrimitiveTransport({
|
|
@@ -97,14 +97,17 @@ function dataUrlToBytes(dataUrl: string): {
|
|
|
97
97
|
}
|
|
98
98
|
return { bytes, mediaType };
|
|
99
99
|
}
|
|
100
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
bytes: new TextEncoder().encode(decodeURIComponent(payload)),
|
|
102
|
+
mediaType,
|
|
103
|
+
};
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
/** Encode raw bytes as a `data:<mediaType>;base64,...` URL. */
|
|
104
107
|
function bytesToDataUrl(bytes: Uint8Array, mediaType: string): string {
|
|
105
108
|
let binary = "";
|
|
106
109
|
// Chunk to stay well under the argument-count ceiling of String.fromCharCode.
|
|
107
|
-
const chunk =
|
|
110
|
+
const chunk = 0x80_00;
|
|
108
111
|
for (let i = 0; i < bytes.length; i += chunk) {
|
|
109
112
|
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
110
113
|
}
|
|
@@ -236,9 +239,7 @@ export function httpPrimitiveTransport(
|
|
|
236
239
|
data?: Array<{ url?: string; b64_json?: string }>;
|
|
237
240
|
};
|
|
238
241
|
return (parsed.data ?? []).map((item) =>
|
|
239
|
-
item.url
|
|
240
|
-
? item.url
|
|
241
|
-
: `data:image/png;base64,${item.b64_json ?? ""}`
|
|
242
|
+
item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
|
|
242
243
|
);
|
|
243
244
|
};
|
|
244
245
|
|
package/src/runnable/tool.ts
CHANGED
|
@@ -153,9 +153,6 @@ export interface ToolRunnable<
|
|
|
153
153
|
TInput extends Record<string, unknown> = Record<string, unknown>,
|
|
154
154
|
TOutput = unknown,
|
|
155
155
|
> extends Runnable<TInput, TOutput> {
|
|
156
|
-
readonly kind: "tool";
|
|
157
|
-
/** JSON Schema for this tool's input — compatible with Core's ToolInfo.schema. */
|
|
158
|
-
readonly schema: ToolSchema;
|
|
159
156
|
/**
|
|
160
157
|
* The `run` body serialized for Core's `inline_deno` tool backend — the exact
|
|
161
158
|
* same technique `defineTurnHook` uses for its `code`. This is what makes a
|
|
@@ -174,6 +171,9 @@ export interface ToolRunnable<
|
|
|
174
171
|
* the sandbox form is the second parameter aliased to `host`.
|
|
175
172
|
*/
|
|
176
173
|
readonly code: string;
|
|
174
|
+
readonly kind: "tool";
|
|
175
|
+
/** JSON Schema for this tool's input — compatible with Core's ToolInfo.schema. */
|
|
176
|
+
readonly schema: ToolSchema;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
@@ -232,7 +232,7 @@ export function defineTool<
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
|
-
* Convert a {@link ToolRunnable} into a `
|
|
235
|
+
* Convert a {@link ToolRunnable} into a `manifest.json` `kind:"tool"` runnable that
|
|
236
236
|
* ships its `run` body as Core's `inline_deno` backend. The emitted config
|
|
237
237
|
* mirrors Core's `ToolConfig` (`apps/core/src/plugin_manifest/schema.rs`):
|
|
238
238
|
* `{ slug, backend:"inline_deno", code, description?, input_schema }`. Core
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type {
|
|
17
17
|
Contributes,
|
|
18
|
+
HookEventContribution,
|
|
18
19
|
PluginManifest,
|
|
19
20
|
RunnableMeta,
|
|
20
21
|
Surface,
|
|
@@ -90,9 +91,12 @@ export interface DefineTurnHookOptions {
|
|
|
90
91
|
* is serialized into the sandbox `code` string and invoked with `ctx`/`host` at
|
|
91
92
|
* run time.
|
|
92
93
|
*/
|
|
94
|
+
// `code` is optional on `TurnHookContribution` because a hand-authored manifest may
|
|
95
|
+
// carry `code_file` instead. This builder always produces the inline form, so it
|
|
96
|
+
// narrows the return type — callers get a `code` they need not null-check.
|
|
93
97
|
export function defineTurnHook(
|
|
94
98
|
options: DefineTurnHookOptions
|
|
95
|
-
): TurnHookContribution {
|
|
99
|
+
): TurnHookContribution & { code: string } {
|
|
96
100
|
const source = options.run.toString();
|
|
97
101
|
// The sandbox wraps `code` in an async IIFE where `ctx`/`host` are in scope
|
|
98
102
|
// and a bare `return` reports the directive — so call the serialized function
|
|
@@ -112,8 +116,24 @@ export interface DefinePluginOptions {
|
|
|
112
116
|
composerControls?: Record<string, unknown>[];
|
|
113
117
|
/** Capability grants the hooks need (e.g. `["hook:side-model", "storage:kv"]`). */
|
|
114
118
|
grants?: string[];
|
|
119
|
+
/**
|
|
120
|
+
* App events this plugin EMITS — the provider half of the hook system whose
|
|
121
|
+
* consumer half is {@link DefinePluginOptions.turnHooks}. Each `id` must be
|
|
122
|
+
* namespaced to this plugin's own `id`; Core validates that at load and again
|
|
123
|
+
* on every emit.
|
|
124
|
+
*/
|
|
125
|
+
hookEvents?: HookEventContribution[];
|
|
115
126
|
/** Reverse-domain id (e.g. `"com.example.my-plugin"`). */
|
|
116
127
|
id: string;
|
|
128
|
+
/**
|
|
129
|
+
* Language servers the plugin declares, keyed by server name — the same shape
|
|
130
|
+
* as Claude Code's `lspServers` / `.lsp.json`, passed verbatim. Loose records
|
|
131
|
+
* rather than a typed entry on purpose: Claude Code owns this field
|
|
132
|
+
* vocabulary, so typing it here would strip a field from a newer Claude
|
|
133
|
+
* release on its way through `ryu pack`. Core types it, because Core acts on
|
|
134
|
+
* it.
|
|
135
|
+
*/
|
|
136
|
+
lspServers?: Record<string, Record<string, unknown>>;
|
|
117
137
|
/** Display name. */
|
|
118
138
|
name: string;
|
|
119
139
|
/**
|
|
@@ -144,19 +164,28 @@ export interface DefinePluginOptions {
|
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
/**
|
|
147
|
-
* Assemble a `
|
|
167
|
+
* Assemble a `manifest.json` manifest for a turn-hook plugin. The result matches
|
|
148
168
|
* Core's `PluginManifest` serde shape and can be written to disk or validated via
|
|
149
169
|
* `validateManifestStrict`.
|
|
150
170
|
*/
|
|
151
171
|
export function definePlugin(options: DefinePluginOptions): PluginManifest {
|
|
152
172
|
const contributes: Contributes = {
|
|
153
173
|
turn_hooks: options.turnHooks ?? [],
|
|
174
|
+
hook_events: options.hookEvents ?? [],
|
|
154
175
|
composer_controls: options.composerControls ?? [],
|
|
155
176
|
settings_tabs: options.settingsTabs ?? [],
|
|
156
177
|
slash_commands: options.slashCommands ?? [],
|
|
157
|
-
|
|
158
|
-
//
|
|
178
|
+
lsp_servers: options.lspServers ?? {},
|
|
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.
|
|
159
183
|
widgets: [],
|
|
184
|
+
sidebar_sections: [],
|
|
185
|
+
sidebar_buttons: [],
|
|
186
|
+
dock_panels: [],
|
|
187
|
+
data_categories: [],
|
|
188
|
+
pi_extensions: [],
|
|
160
189
|
};
|
|
161
190
|
// Ship each inline tool as a `kind:"tool"` runnable (Core's `inline_deno`
|
|
162
191
|
// backend). Shipping tools requires the `tool:execute` grant; add it once.
|