@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.
@@ -0,0 +1,210 @@
1
+ // src/agent-plugin.ts
2
+ var AGENT_PLUGINS_SPEC_VERSION = "1.0.0";
3
+ var AGENT_PLUGIN_SCHEMA_URL = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
4
+ var AGENT_PLUGIN_MCP_SCHEMA_URL = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
5
+ var AGENT_PLUGIN_EXTENSION_NS = "com.ryuhq.ryu";
6
+ var AGENT_PLUGIN_MANIFEST_FILE = "plugin.json";
7
+ var AGENT_PLUGIN_MCP_FILE = "mcp.json";
8
+ function isAgentPluginManifest(value) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10
+ return false;
11
+ }
12
+ const schema = value.$schema;
13
+ return typeof schema === "string" && schema.startsWith("https://agent-plugins.org/schemas/");
14
+ }
15
+ var SPEC_NAME_MAX = 64;
16
+ var ILLEGAL_NAME_CHARS = /[^a-z0-9.-]+/g;
17
+ var REPEATED_HYPHENS = /-{2,}/g;
18
+ var REPEATED_DOTS = /\.{2,}/g;
19
+ var LEADING_NON_ALNUM = /^[^a-z0-9]+/;
20
+ var TRAILING_NON_ALNUM = /[^a-z0-9]+$/;
21
+ var WHITESPACE = /\s/;
22
+ function toSpecName(id) {
23
+ const normalized = id.trim().toLowerCase().replace(/^@/, "").replace(/[/_]/g, ".").replace(ILLEGAL_NAME_CHARS, "-").replace(REPEATED_HYPHENS, "-").replace(REPEATED_DOTS, ".").replace(LEADING_NON_ALNUM, "").replace(TRAILING_NON_ALNUM, "").slice(0, SPEC_NAME_MAX).replace(TRAILING_NON_ALNUM, "");
24
+ if (!normalized) {
25
+ throw new Error(
26
+ `plugin id ${JSON.stringify(id)} has no spec-legal name projection`
27
+ );
28
+ }
29
+ return normalized;
30
+ }
31
+ function asString(value) {
32
+ return typeof value === "string" && value.trim() ? value : void 0;
33
+ }
34
+ function asStringArray(value) {
35
+ if (!Array.isArray(value)) {
36
+ return;
37
+ }
38
+ const strings = value.filter((v) => typeof v === "string");
39
+ return strings.length > 0 ? strings : void 0;
40
+ }
41
+ function toSpecAuthor(value) {
42
+ const bare = asString(value);
43
+ if (bare) {
44
+ return { name: bare };
45
+ }
46
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
47
+ return;
48
+ }
49
+ const source = value;
50
+ const author = {};
51
+ const name = asString(source.name);
52
+ const email = asString(source.email);
53
+ const url = asString(source.url);
54
+ if (name) {
55
+ author.name = name;
56
+ }
57
+ if (email) {
58
+ author.email = email;
59
+ }
60
+ if (url) {
61
+ author.url = url;
62
+ }
63
+ return Object.keys(author).length > 0 ? author : void 0;
64
+ }
65
+ function toSpecEnv(value) {
66
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
67
+ return;
68
+ }
69
+ const env = {};
70
+ for (const [key, raw] of Object.entries(value)) {
71
+ if (typeof raw === "string") {
72
+ env[key] = raw;
73
+ }
74
+ }
75
+ return Object.keys(env).length > 0 ? env : void 0;
76
+ }
77
+ function toSpecServer(name, decl) {
78
+ const extras = {};
79
+ const commandEnv = asString(decl.command_env);
80
+ const description = asString(decl.description);
81
+ if (commandEnv) {
82
+ extras.command_env = commandEnv;
83
+ }
84
+ if (description) {
85
+ extras.description = description;
86
+ }
87
+ if (decl.enabled === false) {
88
+ extras.enabled = false;
89
+ return {
90
+ extras,
91
+ note: `mcp server '${name}' is disabled in the native manifest and was omitted from ${AGENT_PLUGIN_MCP_FILE}`
92
+ };
93
+ }
94
+ const command = asString(decl.command);
95
+ if (!command) {
96
+ return {
97
+ extras,
98
+ note: `mcp server '${name}' has no command and was omitted`
99
+ };
100
+ }
101
+ if (WHITESPACE.test(command)) {
102
+ return {
103
+ extras,
104
+ note: `mcp server '${name}' command ${JSON.stringify(command)} is not a single executable token and was omitted`
105
+ };
106
+ }
107
+ if (command.startsWith("/") || command.startsWith("~")) {
108
+ return {
109
+ extras,
110
+ note: `mcp server '${name}' command ${JSON.stringify(command)} is an absolute path (spec allows a bare name or './' relative path) and was omitted`
111
+ };
112
+ }
113
+ const server = { type: "stdio", command };
114
+ const args = asStringArray(decl.args);
115
+ if (args) {
116
+ server.args = args;
117
+ }
118
+ const env = toSpecEnv(decl.env);
119
+ if (env) {
120
+ server.env = env;
121
+ }
122
+ return { server, extras };
123
+ }
124
+ function toAgentPlugin(manifest) {
125
+ const id = asString(manifest.id);
126
+ if (!id) {
127
+ throw new Error("manifest has no id");
128
+ }
129
+ const notes = [];
130
+ const ryu = {
131
+ id,
132
+ displayName: asString(manifest.name) ?? id
133
+ };
134
+ const plugin = {
135
+ $schema: AGENT_PLUGIN_SCHEMA_URL,
136
+ name: toSpecName(id)
137
+ };
138
+ const version = asString(manifest.version);
139
+ if (version) {
140
+ plugin.version = version;
141
+ }
142
+ const description = asString(manifest.description) ?? asString(manifest.tagline);
143
+ if (description) {
144
+ plugin.description = description;
145
+ }
146
+ const author = toSpecAuthor(manifest.author);
147
+ if (author) {
148
+ plugin.author = author;
149
+ }
150
+ const homepage = asString(manifest.homepage);
151
+ if (homepage) {
152
+ plugin.homepage = homepage;
153
+ }
154
+ const repository = asString(manifest.repository);
155
+ if (repository) {
156
+ plugin.repository = repository;
157
+ }
158
+ const license = asString(manifest.license);
159
+ if (license) {
160
+ plugin.license = license;
161
+ }
162
+ const keywords = asStringArray(manifest.keywords);
163
+ if (keywords) {
164
+ plugin.keywords = keywords;
165
+ }
166
+ const declared = manifest.mcp_servers;
167
+ const servers = {};
168
+ const mcpExtras = {};
169
+ if (declared && typeof declared === "object" && !Array.isArray(declared)) {
170
+ for (const [name, raw] of Object.entries(
171
+ declared
172
+ )) {
173
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
174
+ notes.push(`mcp server '${name}' is not an object and was omitted`);
175
+ continue;
176
+ }
177
+ const { server, extras, note } = toSpecServer(
178
+ name,
179
+ raw
180
+ );
181
+ if (Object.keys(extras).length > 0) {
182
+ mcpExtras[name] = extras;
183
+ }
184
+ if (note) {
185
+ notes.push(note);
186
+ }
187
+ if (server) {
188
+ servers[name] = server;
189
+ }
190
+ }
191
+ }
192
+ if (Object.keys(mcpExtras).length > 0) {
193
+ ryu.mcp = mcpExtras;
194
+ }
195
+ plugin.extensions = { [AGENT_PLUGIN_EXTENSION_NS]: ryu };
196
+ const mcp = Object.keys(servers).length > 0 ? { $schema: AGENT_PLUGIN_MCP_SCHEMA_URL, mcpServers: servers } : null;
197
+ return { plugin, mcp, notes };
198
+ }
199
+
200
+ export {
201
+ AGENT_PLUGINS_SPEC_VERSION,
202
+ AGENT_PLUGIN_SCHEMA_URL,
203
+ AGENT_PLUGIN_MCP_SCHEMA_URL,
204
+ AGENT_PLUGIN_EXTENSION_NS,
205
+ AGENT_PLUGIN_MANIFEST_FILE,
206
+ AGENT_PLUGIN_MCP_FILE,
207
+ isAgentPluginManifest,
208
+ toSpecName,
209
+ toAgentPlugin
210
+ };
package/dist/cli.cjs CHANGED
@@ -28,6 +28,204 @@ var import_node_crypto = require("crypto");
28
28
  var import_node_fs = require("fs");
29
29
  var import_node_path = require("path");
30
30
 
31
+ // src/agent-plugin.ts
32
+ var AGENT_PLUGIN_SCHEMA_URL = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
33
+ var AGENT_PLUGIN_MCP_SCHEMA_URL = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
34
+ var AGENT_PLUGIN_EXTENSION_NS = "com.ryuhq.ryu";
35
+ var AGENT_PLUGIN_MANIFEST_FILE = "plugin.json";
36
+ var AGENT_PLUGIN_MCP_FILE = "mcp.json";
37
+ function isAgentPluginManifest(value) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
39
+ return false;
40
+ }
41
+ const schema = value.$schema;
42
+ return typeof schema === "string" && schema.startsWith("https://agent-plugins.org/schemas/");
43
+ }
44
+ var SPEC_NAME_MAX = 64;
45
+ var ILLEGAL_NAME_CHARS = /[^a-z0-9.-]+/g;
46
+ var REPEATED_HYPHENS = /-{2,}/g;
47
+ var REPEATED_DOTS = /\.{2,}/g;
48
+ var LEADING_NON_ALNUM = /^[^a-z0-9]+/;
49
+ var TRAILING_NON_ALNUM = /[^a-z0-9]+$/;
50
+ var WHITESPACE = /\s/;
51
+ function toSpecName(id) {
52
+ const normalized = id.trim().toLowerCase().replace(/^@/, "").replace(/[/_]/g, ".").replace(ILLEGAL_NAME_CHARS, "-").replace(REPEATED_HYPHENS, "-").replace(REPEATED_DOTS, ".").replace(LEADING_NON_ALNUM, "").replace(TRAILING_NON_ALNUM, "").slice(0, SPEC_NAME_MAX).replace(TRAILING_NON_ALNUM, "");
53
+ if (!normalized) {
54
+ throw new Error(
55
+ `plugin id ${JSON.stringify(id)} has no spec-legal name projection`
56
+ );
57
+ }
58
+ return normalized;
59
+ }
60
+ function asString(value) {
61
+ return typeof value === "string" && value.trim() ? value : void 0;
62
+ }
63
+ function asStringArray(value) {
64
+ if (!Array.isArray(value)) {
65
+ return;
66
+ }
67
+ const strings = value.filter((v) => typeof v === "string");
68
+ return strings.length > 0 ? strings : void 0;
69
+ }
70
+ function toSpecAuthor(value) {
71
+ const bare = asString(value);
72
+ if (bare) {
73
+ return { name: bare };
74
+ }
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76
+ return;
77
+ }
78
+ const source = value;
79
+ const author = {};
80
+ const name = asString(source.name);
81
+ const email = asString(source.email);
82
+ const url = asString(source.url);
83
+ if (name) {
84
+ author.name = name;
85
+ }
86
+ if (email) {
87
+ author.email = email;
88
+ }
89
+ if (url) {
90
+ author.url = url;
91
+ }
92
+ return Object.keys(author).length > 0 ? author : void 0;
93
+ }
94
+ function toSpecEnv(value) {
95
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
96
+ return;
97
+ }
98
+ const env = {};
99
+ for (const [key, raw] of Object.entries(value)) {
100
+ if (typeof raw === "string") {
101
+ env[key] = raw;
102
+ }
103
+ }
104
+ return Object.keys(env).length > 0 ? env : void 0;
105
+ }
106
+ function toSpecServer(name, decl) {
107
+ const extras = {};
108
+ const commandEnv = asString(decl.command_env);
109
+ const description = asString(decl.description);
110
+ if (commandEnv) {
111
+ extras.command_env = commandEnv;
112
+ }
113
+ if (description) {
114
+ extras.description = description;
115
+ }
116
+ if (decl.enabled === false) {
117
+ extras.enabled = false;
118
+ return {
119
+ extras,
120
+ note: `mcp server '${name}' is disabled in the native manifest and was omitted from ${AGENT_PLUGIN_MCP_FILE}`
121
+ };
122
+ }
123
+ const command2 = asString(decl.command);
124
+ if (!command2) {
125
+ return {
126
+ extras,
127
+ note: `mcp server '${name}' has no command and was omitted`
128
+ };
129
+ }
130
+ if (WHITESPACE.test(command2)) {
131
+ return {
132
+ extras,
133
+ note: `mcp server '${name}' command ${JSON.stringify(command2)} is not a single executable token and was omitted`
134
+ };
135
+ }
136
+ if (command2.startsWith("/") || command2.startsWith("~")) {
137
+ return {
138
+ extras,
139
+ note: `mcp server '${name}' command ${JSON.stringify(command2)} is an absolute path (spec allows a bare name or './' relative path) and was omitted`
140
+ };
141
+ }
142
+ const server = { type: "stdio", command: command2 };
143
+ const args2 = asStringArray(decl.args);
144
+ if (args2) {
145
+ server.args = args2;
146
+ }
147
+ const env = toSpecEnv(decl.env);
148
+ if (env) {
149
+ server.env = env;
150
+ }
151
+ return { server, extras };
152
+ }
153
+ function toAgentPlugin(manifest) {
154
+ const id = asString(manifest.id);
155
+ if (!id) {
156
+ throw new Error("manifest has no id");
157
+ }
158
+ const notes = [];
159
+ const ryu = {
160
+ id,
161
+ displayName: asString(manifest.name) ?? id
162
+ };
163
+ const plugin = {
164
+ $schema: AGENT_PLUGIN_SCHEMA_URL,
165
+ name: toSpecName(id)
166
+ };
167
+ const version = asString(manifest.version);
168
+ if (version) {
169
+ plugin.version = version;
170
+ }
171
+ const description = asString(manifest.description) ?? asString(manifest.tagline);
172
+ if (description) {
173
+ plugin.description = description;
174
+ }
175
+ const author = toSpecAuthor(manifest.author);
176
+ if (author) {
177
+ plugin.author = author;
178
+ }
179
+ const homepage = asString(manifest.homepage);
180
+ if (homepage) {
181
+ plugin.homepage = homepage;
182
+ }
183
+ const repository = asString(manifest.repository);
184
+ if (repository) {
185
+ plugin.repository = repository;
186
+ }
187
+ const license = asString(manifest.license);
188
+ if (license) {
189
+ plugin.license = license;
190
+ }
191
+ const keywords = asStringArray(manifest.keywords);
192
+ if (keywords) {
193
+ plugin.keywords = keywords;
194
+ }
195
+ const declared = manifest.mcp_servers;
196
+ const servers = {};
197
+ const mcpExtras = {};
198
+ if (declared && typeof declared === "object" && !Array.isArray(declared)) {
199
+ for (const [name, raw] of Object.entries(
200
+ declared
201
+ )) {
202
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
203
+ notes.push(`mcp server '${name}' is not an object and was omitted`);
204
+ continue;
205
+ }
206
+ const { server, extras, note } = toSpecServer(
207
+ name,
208
+ raw
209
+ );
210
+ if (Object.keys(extras).length > 0) {
211
+ mcpExtras[name] = extras;
212
+ }
213
+ if (note) {
214
+ notes.push(note);
215
+ }
216
+ if (server) {
217
+ servers[name] = server;
218
+ }
219
+ }
220
+ }
221
+ if (Object.keys(mcpExtras).length > 0) {
222
+ ryu.mcp = mcpExtras;
223
+ }
224
+ plugin.extensions = { [AGENT_PLUGIN_EXTENSION_NS]: ryu };
225
+ const mcp = Object.keys(servers).length > 0 ? { $schema: AGENT_PLUGIN_MCP_SCHEMA_URL, mcpServers: servers } : null;
226
+ return { plugin, mcp, notes };
227
+ }
228
+
31
229
  // src/cli/dev.ts
32
230
  var import_node_readline = require("readline");
33
231
 
@@ -404,6 +602,20 @@ var PiExtensionContributionSchema = import_zod.z.object({
404
602
  /** Optional one-liner describing what the extension adds to the agent. */
405
603
  description: import_zod.z.string().optional()
406
604
  });
605
+ var OutputStyleContributionSchema = import_zod.z.object({
606
+ /** Stable id for this style within the plugin (`[a-z0-9][a-z0-9._-]*`). It is
607
+ * also the persisted selection key, so it must survive a settings key and a URL
608
+ * path. */
609
+ id: import_zod.z.string().min(1),
610
+ /** SOURCE form: path to the Markdown file, relative to the plugin root —
611
+ * exactly `output-styles/<name>.md`. `ryu pack` replaces this with `source`. */
612
+ file: import_zod.z.string().min(1).optional(),
613
+ /** WIRE form: the file's contents verbatim, frontmatter INCLUDED. The whole file
614
+ * rather than a pre-split body plus mirrored `name`/`description` keys, so a
615
+ * plugin style and a user's own `output-styles/*.md` go through one parser and
616
+ * the frontmatter stays the single source of truth for a style's metadata. */
617
+ source: import_zod.z.string().optional()
618
+ });
407
619
  var DEFAULT_WIDGET_MIME = "text/html+skybridge";
408
620
  var DEFAULT_WIDGET_DISPLAY_MODE = "inline";
409
621
  var WidgetContributionSchema = import_zod.z.object({
@@ -494,7 +706,15 @@ var ContributesSchema = import_zod.z.object({
494
706
  * Typed (not a loose record) because Ryu owns this vocabulary — three fields,
495
707
  * all of them Core-interpreted — unlike `lsp_servers`, whose entry shape is
496
708
  * Claude Code's to extend. */
497
- pi_extensions: import_zod.z.array(PiExtensionContributionSchema).default([])
709
+ pi_extensions: import_zod.z.array(PiExtensionContributionSchema).default([]),
710
+ /** Output styles the plugin ships — Markdown files that rewrite the system
711
+ * prompt's voice. Mirrors the Rust-side `Contributes.output_styles`; without it
712
+ * the CLI's zod parse would strip the declaration, and `ryu pack` would sign a
713
+ * bundle whose styles simply do not exist. Worse than the usual case of that
714
+ * bug: the styles' `.md` files are not carried by the bundle either, so there
715
+ * would be no residue to notice — the plugin would install clean and contribute
716
+ * nothing. */
717
+ output_styles: import_zod.z.array(OutputStyleContributionSchema).default([])
498
718
  });
499
719
  var SetupStepSchema = import_zod.z.object({
500
720
  /** Card heading (e.g. the companion app name). */
@@ -728,6 +948,8 @@ function printUsage() {
728
948
  " bunx ryu pack <dir> Validate and bundle a manifest.json Plugin",
729
949
  " bunx ryu publish <dir> Validate and publish a manifest.json Plugin to the Ryu Marketplace",
730
950
  " bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
951
+ " bunx ryu agent-plugin <dir>",
952
+ " Emit the Agent Plugins v1 interop pair (plugin.json + mcp.json)",
731
953
  ""
732
954
  ].join("\n")
733
955
  );
@@ -742,10 +964,20 @@ var MANIFEST_FILE_NAMES = [
742
964
  "plugin.json",
743
965
  "ryu.json"
744
966
  ];
967
+ function resolveNativeManifestPath(dir) {
968
+ return MANIFEST_FILE_NAMES.map((name) => (0, import_node_path.join)(dir, name)).find((candidate) => {
969
+ if (!(0, import_node_fs.existsSync)(candidate)) {
970
+ return false;
971
+ }
972
+ try {
973
+ return !isAgentPluginManifest(JSON.parse((0, import_node_fs.readFileSync)(candidate, "utf8")));
974
+ } catch {
975
+ return true;
976
+ }
977
+ });
978
+ }
745
979
  function loadManifest(dir) {
746
- const manifestPath = MANIFEST_FILE_NAMES.map((name) => (0, import_node_path.join)(dir, name)).find(
747
- (candidate) => (0, import_node_fs.existsSync)(candidate)
748
- );
980
+ const manifestPath = resolveNativeManifestPath(dir);
749
981
  if (!manifestPath) {
750
982
  exitError(`manifest.json not found in: ${dir}`);
751
983
  }
@@ -768,7 +1000,7 @@ function loadManifest(dir) {
768
1000
  const message = first?.message ?? "validation failed";
769
1001
  exitError(`manifest.json validation failed at '${field}': ${message}`);
770
1002
  }
771
- return inlineCodeFiles(result.data, dir);
1003
+ return inlineOutputStyleFiles(inlineCodeFiles(result.data, dir), dir);
772
1004
  }
773
1005
  var CODE_FILE_DIRS = ["hooks", "adapters"];
774
1006
  var CODE_FILE_PATH = /^(hooks|adapters)\/[A-Za-z0-9_][A-Za-z0-9._-]*\.m?js$/;
@@ -810,6 +1042,42 @@ function inlineCodeFiles(manifest, dir) {
810
1042
  }
811
1043
  return out;
812
1044
  }
1045
+ var MAX_OUTPUT_STYLE_BYTES = 64 * 1024;
1046
+ var OUTPUT_STYLE_DIR = "output-styles";
1047
+ var OUTPUT_STYLE_PATH = /^output-styles\/[A-Za-z0-9_-][A-Za-z0-9._-]*\.md$/;
1048
+ function inlineOutputStyleFiles(manifest, dir) {
1049
+ const out = manifest;
1050
+ for (const style of out.contributes?.output_styles ?? []) {
1051
+ const rel = style.file;
1052
+ if (typeof rel !== "string") {
1053
+ continue;
1054
+ }
1055
+ const label = `output style '${String(style.id)}'`;
1056
+ if (!OUTPUT_STYLE_PATH.test(rel) || rel.includes("..")) {
1057
+ exitError(
1058
+ `${label}: file '${rel}' must be exactly '${OUTPUT_STYLE_DIR}/<name>.md' with no traversal`
1059
+ );
1060
+ }
1061
+ let body;
1062
+ try {
1063
+ body = (0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, rel), "utf8");
1064
+ } catch (err) {
1065
+ exitError(`${label}: could not read file '${rel}': ${String(err)}`);
1066
+ }
1067
+ if (!body.trim()) {
1068
+ exitError(`${label}: file '${rel}' is empty`);
1069
+ }
1070
+ const bytes = Buffer.byteLength(body, "utf8");
1071
+ if (bytes > MAX_OUTPUT_STYLE_BYTES) {
1072
+ exitError(
1073
+ `${label}: file '${rel}' is ${bytes} bytes, over the ${MAX_OUTPUT_STYLE_BYTES}-byte limit`
1074
+ );
1075
+ }
1076
+ style.source = body;
1077
+ style.file = void 0;
1078
+ }
1079
+ return out;
1080
+ }
813
1081
  function resolveUiEntry(manifest) {
814
1082
  for (const runnable of manifest.runnables) {
815
1083
  if (runnable.kind !== "companion") {
@@ -868,6 +1136,48 @@ async function bundleUiEntry(dir, uiEntry) {
868
1136
  }
869
1137
  return await output.text();
870
1138
  }
1139
+ function emitAgentPlugin(dir) {
1140
+ const manifestPath = resolveNativeManifestPath(dir);
1141
+ if (!manifestPath) {
1142
+ exitError(`manifest.json not found in: ${dir}`);
1143
+ }
1144
+ let manifest;
1145
+ try {
1146
+ manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
1147
+ } catch (err) {
1148
+ exitError(`could not read ${manifestPath}: ${String(err)}`);
1149
+ }
1150
+ let plugin;
1151
+ let mcp;
1152
+ let notes;
1153
+ try {
1154
+ ({ plugin, mcp, notes } = toAgentPlugin(manifest));
1155
+ } catch (err) {
1156
+ exitError(`could not derive ${AGENT_PLUGIN_MANIFEST_FILE}: ${String(err)}`);
1157
+ }
1158
+ const pluginPath = (0, import_node_path.join)(dir, AGENT_PLUGIN_MANIFEST_FILE);
1159
+ (0, import_node_fs.writeFileSync)(pluginPath, `${JSON.stringify(plugin, null, 2)}
1160
+ `, "utf8");
1161
+ const mcpPath = (0, import_node_path.join)(dir, AGENT_PLUGIN_MCP_FILE);
1162
+ if (mcp) {
1163
+ (0, import_node_fs.writeFileSync)(mcpPath, `${JSON.stringify(mcp, null, 2)}
1164
+ `, "utf8");
1165
+ } else if ((0, import_node_fs.existsSync)(mcpPath)) {
1166
+ (0, import_node_fs.rmSync)(mcpPath);
1167
+ }
1168
+ const emitted = mcp ? `${AGENT_PLUGIN_MANIFEST_FILE} + ${AGENT_PLUGIN_MCP_FILE}` : AGENT_PLUGIN_MANIFEST_FILE;
1169
+ process.stdout.write(`agent-plugin: ${emitted} \u2192 ${dir}
1170
+ `);
1171
+ for (const note of notes) {
1172
+ process.stdout.write(`agent-plugin: note: ${note}
1173
+ `);
1174
+ }
1175
+ }
1176
+ function commandAgentPlugin(rawDir) {
1177
+ const dir = (0, import_node_path.resolve)(rawDir);
1178
+ loadManifest(dir);
1179
+ emitAgentPlugin(dir);
1180
+ }
871
1181
  async function commandPack(rawDir) {
872
1182
  const dir = (0, import_node_path.resolve)(rawDir);
873
1183
  const manifest = loadManifest(dir);
@@ -881,6 +1191,7 @@ async function commandPack(rawDir) {
881
1191
  const outPath = (0, import_node_path.join)(outDir, "plugin.bundle.json");
882
1192
  const bundle = uiCode ? { ...manifestWithHash, ui_code: uiCode } : manifestWithHash;
883
1193
  (0, import_node_fs.writeFileSync)(outPath, JSON.stringify(bundle, null, 2), "utf8");
1194
+ emitAgentPlugin(dir);
884
1195
  const codeNote = uiCode ? ` (+${uiCode.length}B ui_code)` : "";
885
1196
  process.stdout.write(
886
1197
  `packed ${manifest.id}@${manifest.version}${codeNote} \u2192 ${outPath}
@@ -997,6 +1308,14 @@ if (command === "pack") {
997
1308
  commandPublish(dir).catch((err) => {
998
1309
  exitError(String(err));
999
1310
  });
1311
+ } else if (command === "agent-plugin") {
1312
+ const dir = args[0];
1313
+ if (!dir) {
1314
+ exitError(
1315
+ "agent-plugin requires a directory argument: bunx ryu agent-plugin <dir>"
1316
+ );
1317
+ }
1318
+ commandAgentPlugin(dir);
1000
1319
  } else if (command === "dev") {
1001
1320
  const entry = args[0];
1002
1321
  if (!entry) {