@ryuhq/sdk 0.0.5 → 0.0.17

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/src/cli.ts CHANGED
@@ -7,10 +7,10 @@
7
7
  * bunx ryu publish <dir>
8
8
  *
9
9
  * Commands:
10
- * pack <dir> Validate the plugin.json in <dir> and emit a publish-ready
10
+ * pack <dir> Validate the manifest.json in <dir> and emit a publish-ready
11
11
  * Plugin bundle at <dir>/dist/plugin.bundle.json.
12
12
  * Exits 0 on success; exits 1 with the failing field on error.
13
- * publish <dir> Validate the plugin.json and POST it to the Ryu Marketplace
13
+ * publish <dir> Validate the manifest.json and POST it to the Ryu Marketplace
14
14
  * publish endpoint with the author's auth token. The item is
15
15
  * stored as `pending` until a moderator approves it.
16
16
  */
@@ -38,8 +38,8 @@ function printUsage(): void {
38
38
  "Ryu dev SDK",
39
39
  "",
40
40
  "Usage:",
41
- " bunx ryu pack <dir> Validate and bundle a plugin.json Plugin",
42
- " bunx ryu publish <dir> Validate and publish a plugin.json Plugin to the Ryu Marketplace",
41
+ " bunx ryu pack <dir> Validate and bundle a manifest.json Plugin",
42
+ " bunx ryu publish <dir> Validate and publish a manifest.json Plugin to the Ryu Marketplace",
43
43
  " bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
44
44
  "",
45
45
  ].join("\n")
@@ -55,12 +55,24 @@ function exitError(message: string): never {
55
55
 
56
56
  type LoadedManifest = ReturnType<typeof PluginManifestSchema.parse>;
57
57
 
58
- // Read + parse + validate the plugin.json in `dir`. Exits with the failing
58
+ // Manifest file names, in preference order mirrors Core's resolver
59
+ // (`plugin_manifest::MANIFEST_FILE_NAMES`). `manifest.json` is canonical; the
60
+ // legacy `plugin.json` / `ryu.json` are still accepted so an author's existing
61
+ // project directory keeps packing without a rename.
62
+ const MANIFEST_FILE_NAMES = [
63
+ "manifest.json",
64
+ "plugin.json",
65
+ "ryu.json",
66
+ ] as const;
67
+
68
+ // Read + parse + validate the manifest in `dir`. Exits with the failing
59
69
  // field on any error. Shared by pack and publish so both validate identically.
60
70
  function loadManifest(dir: string): LoadedManifest {
61
- const manifestPath = join(dir, "plugin.json");
62
- if (!existsSync(manifestPath)) {
63
- exitError(`plugin.json not found in: ${dir}`);
71
+ const manifestPath = MANIFEST_FILE_NAMES.map((name) => join(dir, name)).find(
72
+ (candidate) => existsSync(candidate)
73
+ );
74
+ if (!manifestPath) {
75
+ exitError(`manifest.json not found in: ${dir}`);
64
76
  }
65
77
 
66
78
  let raw: string;
@@ -74,7 +86,7 @@ function loadManifest(dir: string): LoadedManifest {
74
86
  try {
75
87
  parsed = JSON.parse(raw);
76
88
  } catch {
77
- exitError(`plugin.json is not valid JSON: ${manifestPath}`);
89
+ exitError(`manifest.json is not valid JSON: ${manifestPath}`);
78
90
  }
79
91
 
80
92
  const result = PluginManifestSchema.safeParse(parsed);
@@ -82,9 +94,82 @@ function loadManifest(dir: string): LoadedManifest {
82
94
  const first = result.error.issues[0];
83
95
  const field = first?.path.join(".") ?? "unknown";
84
96
  const message = first?.message ?? "validation failed";
85
- exitError(`plugin.json validation failed at '${field}': ${message}`);
97
+ exitError(`manifest.json validation failed at '${field}': ${message}`);
98
+ }
99
+ return inlineCodeFiles(result.data, dir);
100
+ }
101
+
102
+ /** Directories a `code_file` may name — mirrors Rust's `CODE_FILE_DIRS`. */
103
+ const CODE_FILE_DIRS = ["hooks", "adapters"];
104
+ const CODE_FILE_PATH = /^(hooks|adapters)\/[A-Za-z0-9_][A-Za-z0-9._-]*\.m?js$/;
105
+
106
+ /**
107
+ * Read one `code_file` and return its contents, or exit with a clear error.
108
+ *
109
+ * The path is joined onto the plugin directory, so it is validated against the
110
+ * same flat allowlist Core enforces (`<hooks|adapters>/<name>.js`, no traversal)
111
+ * rather than trusted.
112
+ */
113
+ function readCodeFile(dir: string, rel: string, label: string): string {
114
+ if (!CODE_FILE_PATH.test(rel)) {
115
+ exitError(
116
+ `${label}: code_file '${rel}' must be exactly '<${CODE_FILE_DIRS.join("|")}>/<name>.js' with no traversal`
117
+ );
118
+ }
119
+ const path = join(dir, rel);
120
+ let body: string;
121
+ try {
122
+ body = readFileSync(path, "utf8");
123
+ } catch (err) {
124
+ exitError(`${label}: could not read code_file '${rel}': ${String(err)}`);
125
+ }
126
+ if (!body.trim()) {
127
+ exitError(`${label}: code_file '${rel}' is empty`);
86
128
  }
87
- return result.data;
129
+ return body;
130
+ }
131
+
132
+ /**
133
+ * Replace every `code_file` reference with the file's contents — the source form
134
+ * becoming the wire form.
135
+ *
136
+ * `code_file` exists so a plugin's sandboxed JS lives in real, reviewable `.js`
137
+ * files instead of a one-line escaped JSON string. But the BUNDLE must stay
138
+ * self-contained and, for a marketplace plugin, the Gateway signs the manifest
139
+ * verbatim — so inlining here is what keeps the entire hook/adapter body inside the
140
+ * signed surface. A published bundle that still carried `code_file` would be a new
141
+ * unsigned-code carriage channel, which is exactly what this must not become.
142
+ */
143
+ function inlineCodeFiles(
144
+ manifest: LoadedManifest,
145
+ dir: string
146
+ ): LoadedManifest {
147
+ const out = manifest as LoadedManifest & {
148
+ contributes?: { turn_hooks?: Record<string, unknown>[] };
149
+ provides?: {
150
+ tools?: Record<string, { adapter?: Record<string, unknown> }>;
151
+ }[];
152
+ };
153
+
154
+ for (const hook of out.contributes?.turn_hooks ?? []) {
155
+ const rel = hook.code_file;
156
+ if (typeof rel === "string") {
157
+ hook.code = readCodeFile(dir, rel, `turn hook '${String(hook.id)}'`);
158
+ hook.code_file = undefined;
159
+ }
160
+ }
161
+
162
+ for (const entry of out.provides ?? []) {
163
+ for (const [verb, binding] of Object.entries(entry.tools ?? {})) {
164
+ const rel = binding.adapter?.code_file;
165
+ if (binding.adapter && typeof rel === "string") {
166
+ binding.adapter.code = readCodeFile(dir, rel, `adapter '${verb}'`);
167
+ binding.adapter.code_file = undefined;
168
+ }
169
+ }
170
+ }
171
+
172
+ return out;
88
173
  }
89
174
 
90
175
  // ── pack command ──────────────────────────────────────────────────────────────
@@ -236,10 +321,10 @@ function authToken(): string {
236
321
  return token;
237
322
  }
238
323
 
239
- // An SDK-authored Plugin always publishes as a `plugin` (a plugin.json bundle of
324
+ // An SDK-authored Plugin always publishes as a `plugin` (a manifest.json bundle of
240
325
  // runnables). It is deliberately NOT published as `skill`: Core's skill install
241
326
  // path needs a `descriptor.raw.install_source` (a from-source owner/repo), which
242
- // a plugin.json manifest does not carry, so a skill-kind publish would be
327
+ // a manifest.json manifest does not carry, so a skill-kind publish would be
243
328
  // uninstallable. Model / mcp items are published through their own tools.
244
329
  const SDK_PUBLISH_KIND = "plugin" as const;
245
330
 
@@ -8,7 +8,7 @@
8
8
  * that the SDK's deliberately simpler zod authoring schema, the blessed JSON
9
9
  * Schema, and the generated types stay describing the same manifest:
10
10
  *
11
- * 1. a known-good repo manifest (apps-store/mail/ui/plugin.json — the first
11
+ * 1. a known-good repo manifest (apps-store/mail/manifest.json — the first
12
12
  * fully manifest-driven app) parses with the zod `PluginManifestSchema`;
13
13
  * 2. every key the schema marks required exists in the schema, the generated
14
14
  * TS, and the fixture;
@@ -24,7 +24,7 @@ import { PluginManifestSchema } from "./manifest";
24
24
 
25
25
  const FIXTURE_PATH = join(
26
26
  import.meta.dir,
27
- "../../../apps-store/mail/ui/plugin.json"
27
+ "../../../apps-store/mail/manifest.json"
28
28
  );
29
29
  const SCHEMA_PATH = join(
30
30
  import.meta.dir,
@@ -44,9 +44,9 @@ const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf8")) as {
44
44
  const generatedSource = readFileSync(GENERATED_PATH, "utf8");
45
45
 
46
46
  describe("contracts lockstep (zod ↔ blessed JSON Schema ↔ generated TS)", () => {
47
- test("the known-good mail plugin.json parses with the zod authoring schema", () => {
47
+ test("the known-good mail manifest.json parses with the zod authoring schema", () => {
48
48
  const parsed = PluginManifestSchema.parse(fixture);
49
- expect(parsed.id).toBe("com.ryu.mail");
49
+ expect(parsed.id).toBe("@ryu/mail");
50
50
  expect(parsed.runnables.length).toBeGreaterThan(0);
51
51
  });
52
52