@ryuhq/sdk 0.1.4 → 0.1.6
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
|
@@ -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 =
|
|
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) {
|