@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
package/src/cli.ts
CHANGED
|
@@ -5,19 +5,36 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* bunx ryu pack <dir>
|
|
7
7
|
* bunx ryu publish <dir>
|
|
8
|
+
* bunx ryu agent-plugin <dir>
|
|
8
9
|
*
|
|
9
10
|
* Commands:
|
|
10
11
|
* pack <dir> Validate the manifest.json in <dir> and emit a publish-ready
|
|
11
|
-
* Plugin bundle at <dir>/dist/plugin.bundle.json
|
|
12
|
+
* Plugin bundle at <dir>/dist/plugin.bundle.json, plus the
|
|
13
|
+
* Agent Plugins interop pair in <dir> itself.
|
|
12
14
|
* Exits 0 on success; exits 1 with the failing field on error.
|
|
13
15
|
* publish <dir> Validate the manifest.json and POST it to the Ryu Marketplace
|
|
14
16
|
* publish endpoint with the author's auth token. The item is
|
|
15
17
|
* stored as `pending` until a moderator approves it.
|
|
18
|
+
* agent-plugin <dir>
|
|
19
|
+
* Emit only the Agent Plugins v1 interop pair (plugin.json and,
|
|
20
|
+
* when servers exist, mcp.json) derived from the manifest.json.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
import { createHash } from "node:crypto";
|
|
19
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs";
|
|
20
31
|
import { join, resolve } from "node:path";
|
|
32
|
+
import {
|
|
33
|
+
AGENT_PLUGIN_MANIFEST_FILE,
|
|
34
|
+
AGENT_PLUGIN_MCP_FILE,
|
|
35
|
+
isAgentPluginManifest,
|
|
36
|
+
toAgentPlugin,
|
|
37
|
+
} from "./agent-plugin.ts";
|
|
21
38
|
import { commandDev } from "./cli/dev.ts";
|
|
22
39
|
import { PluginManifestSchema } from "./manifest.ts";
|
|
23
40
|
|
|
@@ -41,6 +58,8 @@ function printUsage(): void {
|
|
|
41
58
|
" bunx ryu pack <dir> Validate and bundle a manifest.json Plugin",
|
|
42
59
|
" bunx ryu publish <dir> Validate and publish a manifest.json Plugin to the Ryu Marketplace",
|
|
43
60
|
" bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
|
|
61
|
+
" bunx ryu agent-plugin <dir>",
|
|
62
|
+
" Emit the Agent Plugins v1 interop pair (plugin.json + mcp.json)",
|
|
44
63
|
"",
|
|
45
64
|
].join("\n")
|
|
46
65
|
);
|
|
@@ -65,12 +84,30 @@ const MANIFEST_FILE_NAMES = [
|
|
|
65
84
|
"ryu.json",
|
|
66
85
|
] as const;
|
|
67
86
|
|
|
87
|
+
// Resolve the NATIVE manifest path in `dir`, skipping an exported Agent Plugins
|
|
88
|
+
// `plugin.json`. Since `ryu pack` now writes a spec `plugin.json` into the plugin
|
|
89
|
+
// root, and `plugin.json` is also a legacy alias for our own manifest, a plain
|
|
90
|
+
// first-match would resolve to the spec file in any directory that has no
|
|
91
|
+
// `manifest.json` — and then fail validation for missing `id`. The `$schema`
|
|
92
|
+
// discriminator separates them (see `isAgentPluginManifest`).
|
|
93
|
+
function resolveNativeManifestPath(dir: string): string | undefined {
|
|
94
|
+
return MANIFEST_FILE_NAMES.map((name) => join(dir, name)).find((candidate) => {
|
|
95
|
+
if (!existsSync(candidate)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return !isAgentPluginManifest(JSON.parse(readFileSync(candidate, "utf8")));
|
|
100
|
+
} catch {
|
|
101
|
+
// Unparseable: let the caller surface the JSON error against this path.
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
// Read + parse + validate the manifest in `dir`. Exits with the failing
|
|
69
108
|
// field on any error. Shared by pack and publish so both validate identically.
|
|
70
109
|
function loadManifest(dir: string): LoadedManifest {
|
|
71
|
-
const manifestPath =
|
|
72
|
-
(candidate) => existsSync(candidate)
|
|
73
|
-
);
|
|
110
|
+
const manifestPath = resolveNativeManifestPath(dir);
|
|
74
111
|
if (!manifestPath) {
|
|
75
112
|
exitError(`manifest.json not found in: ${dir}`);
|
|
76
113
|
}
|
|
@@ -96,7 +133,7 @@ function loadManifest(dir: string): LoadedManifest {
|
|
|
96
133
|
const message = first?.message ?? "validation failed";
|
|
97
134
|
exitError(`manifest.json validation failed at '${field}': ${message}`);
|
|
98
135
|
}
|
|
99
|
-
return inlineCodeFiles(result.data, dir);
|
|
136
|
+
return inlineOutputStyleFiles(inlineCodeFiles(result.data, dir), dir);
|
|
100
137
|
}
|
|
101
138
|
|
|
102
139
|
/** Directories a `code_file` may name — mirrors Rust's `CODE_FILE_DIRS`. */
|
|
@@ -172,6 +209,84 @@ function inlineCodeFiles(
|
|
|
172
209
|
return out;
|
|
173
210
|
}
|
|
174
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Largest output-style file `pack` will inline, in bytes — mirrors Rust's
|
|
214
|
+
* `MAX_OUTPUT_STYLE_BYTES`. Enforced here and not left to Core because the two
|
|
215
|
+
* bound different moments: Core rejects an oversized style at install, by which
|
|
216
|
+
* point the author has already signed and published a bundle nobody can install.
|
|
217
|
+
*/
|
|
218
|
+
const MAX_OUTPUT_STYLE_BYTES = 64 * 1024;
|
|
219
|
+
|
|
220
|
+
/** The one directory an `output_styles[].file` may name — mirrors Rust's `OUTPUT_STYLE_DIR`. */
|
|
221
|
+
const OUTPUT_STYLE_DIR = "output-styles";
|
|
222
|
+
/**
|
|
223
|
+
* Mirrors `validate_output_style_path` CHARACTER FOR CHARACTER, unlike
|
|
224
|
+
* `CODE_FILE_PATH` above, which is only morally equivalent to its Rust twin. The
|
|
225
|
+
* two allowlists must accept the same set or `pack` signs a bundle Core rejects at
|
|
226
|
+
* install — the same after-the-fact failure the byte cap below exists to prevent.
|
|
227
|
+
* Hence the leading class excludes `.` but keeps `-` (Rust rejects only a leading
|
|
228
|
+
* dot), and `..` is checked separately rather than folded in, because a dot is
|
|
229
|
+
* legal mid-name and only the doubled form is traversal.
|
|
230
|
+
*/
|
|
231
|
+
const OUTPUT_STYLE_PATH = /^output-styles\/[A-Za-z0-9_-][A-Za-z0-9._-]*\.md$/;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Replace every `output_styles[].file` with the file's contents — the same source
|
|
235
|
+
* form → wire form move `inlineCodeFiles` makes, for the same signing reason.
|
|
236
|
+
*
|
|
237
|
+
* A separate function with a separate allowlist rather than a parameterised version
|
|
238
|
+
* of `readCodeFile`, mirroring why Rust keeps `CODE_FILE_DIRS`, `PI_EXTENSION_DIR`
|
|
239
|
+
* and `OUTPUT_STYLE_DIR` as three constants instead of one: the allowlists ARE the
|
|
240
|
+
* gate, and a merged one is a single edit away from letting a style name a
|
|
241
|
+
* `hooks/*.js`, or a turn hook name a `.md` that nothing sandboxes.
|
|
242
|
+
*
|
|
243
|
+
* The inlined `source` carries the file VERBATIM, frontmatter included — Core's
|
|
244
|
+
* single `parse_output_style_md` reads a plugin style and a user's own
|
|
245
|
+
* `output-styles/*.md` the same way, and mirroring `name`/`description` up into
|
|
246
|
+
* manifest keys would create a second place a style's metadata could disagree with
|
|
247
|
+
* itself.
|
|
248
|
+
*/
|
|
249
|
+
function inlineOutputStyleFiles(
|
|
250
|
+
manifest: LoadedManifest,
|
|
251
|
+
dir: string
|
|
252
|
+
): LoadedManifest {
|
|
253
|
+
const out = manifest as LoadedManifest & {
|
|
254
|
+
contributes?: { output_styles?: Record<string, unknown>[] };
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (const style of out.contributes?.output_styles ?? []) {
|
|
258
|
+
const rel = style.file;
|
|
259
|
+
if (typeof rel !== "string") {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const label = `output style '${String(style.id)}'`;
|
|
263
|
+
if (!OUTPUT_STYLE_PATH.test(rel) || rel.includes("..")) {
|
|
264
|
+
exitError(
|
|
265
|
+
`${label}: file '${rel}' must be exactly '${OUTPUT_STYLE_DIR}/<name>.md' with no traversal`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
let body: string;
|
|
269
|
+
try {
|
|
270
|
+
body = readFileSync(join(dir, rel), "utf8");
|
|
271
|
+
} catch (err) {
|
|
272
|
+
exitError(`${label}: could not read file '${rel}': ${String(err)}`);
|
|
273
|
+
}
|
|
274
|
+
if (!body.trim()) {
|
|
275
|
+
exitError(`${label}: file '${rel}' is empty`);
|
|
276
|
+
}
|
|
277
|
+
const bytes = Buffer.byteLength(body, "utf8");
|
|
278
|
+
if (bytes > MAX_OUTPUT_STYLE_BYTES) {
|
|
279
|
+
exitError(
|
|
280
|
+
`${label}: file '${rel}' is ${bytes} bytes, over the ${MAX_OUTPUT_STYLE_BYTES}-byte limit`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
style.source = body;
|
|
284
|
+
style.file = undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
175
290
|
// ── pack command ──────────────────────────────────────────────────────────────
|
|
176
291
|
|
|
177
292
|
// Resolve the plugin's sandboxed-UI entry module — the source `ryu pack` bundles
|
|
@@ -260,6 +375,72 @@ async function bundleUiEntry(dir: string, uiEntry: string): Promise<string> {
|
|
|
260
375
|
return await output.text();
|
|
261
376
|
}
|
|
262
377
|
|
|
378
|
+
// ── Agent Plugins interop pair ───────────────────────────────────────────────
|
|
379
|
+
//
|
|
380
|
+
// Written into the plugin ROOT, not `dist/`, because the spec addresses a plugin
|
|
381
|
+
// as a directory: `plugin.json` at the root next to `skills/` and `mcp.json`
|
|
382
|
+
// (§4.2). Emitting into `dist/` would produce a manifest with no skills beside it.
|
|
383
|
+
//
|
|
384
|
+
// `manifest.json` remains the source of truth — the pair is derived on every pack,
|
|
385
|
+
// so it cannot drift. A stale `mcp.json` from a previous pack is removed when the
|
|
386
|
+
// manifest no longer declares a server, so an old file can never keep advertising
|
|
387
|
+
// a server we dropped.
|
|
388
|
+
// Reads the manifest RAW rather than taking the validated `LoadedManifest`: the
|
|
389
|
+
// SDK's zod schema models the narrower authoring shape and strips the fields Core
|
|
390
|
+
// adds — including `mcp_servers`, which is exactly what `mcp.json` is derived
|
|
391
|
+
// from. Projecting from the stripped object would silently emit a plugin with no
|
|
392
|
+
// MCP servers.
|
|
393
|
+
function emitAgentPlugin(dir: string): void {
|
|
394
|
+
const manifestPath = resolveNativeManifestPath(dir);
|
|
395
|
+
if (!manifestPath) {
|
|
396
|
+
exitError(`manifest.json not found in: ${dir}`);
|
|
397
|
+
}
|
|
398
|
+
let manifest: Record<string, unknown>;
|
|
399
|
+
try {
|
|
400
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<
|
|
401
|
+
string,
|
|
402
|
+
unknown
|
|
403
|
+
>;
|
|
404
|
+
} catch (err) {
|
|
405
|
+
exitError(`could not read ${manifestPath}: ${String(err)}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let plugin: ReturnType<typeof toAgentPlugin>["plugin"];
|
|
409
|
+
let mcp: ReturnType<typeof toAgentPlugin>["mcp"];
|
|
410
|
+
let notes: string[];
|
|
411
|
+
try {
|
|
412
|
+
({ plugin, mcp, notes } = toAgentPlugin(manifest));
|
|
413
|
+
} catch (err) {
|
|
414
|
+
exitError(`could not derive ${AGENT_PLUGIN_MANIFEST_FILE}: ${String(err)}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const pluginPath = join(dir, AGENT_PLUGIN_MANIFEST_FILE);
|
|
418
|
+
writeFileSync(pluginPath, `${JSON.stringify(plugin, null, 2)}\n`, "utf8");
|
|
419
|
+
|
|
420
|
+
const mcpPath = join(dir, AGENT_PLUGIN_MCP_FILE);
|
|
421
|
+
if (mcp) {
|
|
422
|
+
writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`, "utf8");
|
|
423
|
+
} else if (existsSync(mcpPath)) {
|
|
424
|
+
rmSync(mcpPath);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const emitted = mcp
|
|
428
|
+
? `${AGENT_PLUGIN_MANIFEST_FILE} + ${AGENT_PLUGIN_MCP_FILE}`
|
|
429
|
+
: AGENT_PLUGIN_MANIFEST_FILE;
|
|
430
|
+
process.stdout.write(`agent-plugin: ${emitted} → ${dir}\n`);
|
|
431
|
+
for (const note of notes) {
|
|
432
|
+
process.stdout.write(`agent-plugin: note: ${note}\n`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function commandAgentPlugin(rawDir: string): void {
|
|
437
|
+
const dir = resolve(rawDir);
|
|
438
|
+
// Validate through the normal path first, so `agent-plugin` never emits an
|
|
439
|
+
// interop pair for a manifest `pack`/`publish` would reject.
|
|
440
|
+
loadManifest(dir);
|
|
441
|
+
emitAgentPlugin(dir);
|
|
442
|
+
}
|
|
443
|
+
|
|
263
444
|
async function commandPack(rawDir: string): Promise<void> {
|
|
264
445
|
const dir = resolve(rawDir);
|
|
265
446
|
const manifest = loadManifest(dir);
|
|
@@ -292,6 +473,10 @@ async function commandPack(rawDir: string): Promise<void> {
|
|
|
292
473
|
: manifestWithHash;
|
|
293
474
|
writeFileSync(outPath, JSON.stringify(bundle, null, 2), "utf8");
|
|
294
475
|
|
|
476
|
+
// Re-derive the Agent Plugins interop pair on every pack so a published plugin
|
|
477
|
+
// directory is also a conformant Agent Plugin and the two can never drift.
|
|
478
|
+
emitAgentPlugin(dir);
|
|
479
|
+
|
|
295
480
|
const codeNote = uiCode ? ` (+${uiCode.length}B ui_code)` : "";
|
|
296
481
|
process.stdout.write(
|
|
297
482
|
`packed ${manifest.id}@${manifest.version}${codeNote} → ${outPath}\n`
|
|
@@ -461,6 +646,14 @@ if (command === "pack") {
|
|
|
461
646
|
commandPublish(dir).catch((err: unknown) => {
|
|
462
647
|
exitError(String(err));
|
|
463
648
|
});
|
|
649
|
+
} else if (command === "agent-plugin") {
|
|
650
|
+
const dir = args[0];
|
|
651
|
+
if (!dir) {
|
|
652
|
+
exitError(
|
|
653
|
+
"agent-plugin requires a directory argument: bunx ryu agent-plugin <dir>"
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
commandAgentPlugin(dir);
|
|
464
657
|
} else if (command === "dev") {
|
|
465
658
|
const entry = args[0];
|
|
466
659
|
if (!entry) {
|