0xray 4.0.14 → 4.0.15
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/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +19 -1
- package/.xray/features.json +2 -2
- package/AGENTS-consumer.md +10 -5
- package/AGENTS.md +10 -7
- package/README.md +8 -7
- package/SKILLS.md +3 -2
- package/dist/AGENTS.md +10 -7
- package/dist/CHANGELOG.md +48 -0
- package/dist/README.md +8 -7
- package/dist/SKILLS.md +3 -2
- package/dist/cli/commands/hermes-install.js +2 -1
- package/dist/cli/commands/openclaw-install.js +2 -1
- package/dist/cli/commands/opencode-install.js +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +9 -0
- package/dist/integrations/grok/grok-cli.js +2 -1
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/reporting/log-parser.js +2 -2
- package/dist/skills/orchestrator/SKILL.md +19 -1
- package/llms.txt +8 -7
- package/package.json +2 -1
- package/scripts/foundry/CHANGELOG.md +33 -1
- package/scripts/foundry/README.md +12 -5
- package/scripts/foundry/assert-packed-dist-cli.mjs +6 -0
- package/scripts/foundry/blip-kapow.cjs +2 -0
- package/scripts/foundry/blip-looker.cjs +2 -0
- package/scripts/foundry/blip-render.cjs +2 -1237
- package/scripts/foundry/blip-rippel-organs.cjs +2 -0
- package/scripts/foundry/blip-rippel.cjs +2 -1963
- package/scripts/foundry/blip-scene.cjs +2 -0
- package/scripts/foundry/blip-stroke.cjs +2 -0
- package/scripts/foundry/blip-vibe.cjs +2 -0
- package/scripts/foundry/blip.mjs +23 -97
- package/scripts/foundry/cli.mjs +2 -2
- package/scripts/foundry/inspect.mjs +62 -19
- package/scripts/foundry/mill-plant.cjs +598 -0
- package/scripts/foundry/mint-suit.cjs +189 -99
- package/scripts/foundry/package.json +12 -1
- package/scripts/foundry/plant/agents/blip-inspect.yml +1 -1
- package/scripts/foundry/plant/agents/blip-looker.yml +5 -0
- package/scripts/foundry/plant/agents/blip-vibe.yml +5 -0
- package/scripts/foundry/plant/agents/blip.yml +1 -1
- package/scripts/foundry/plant/agents/inspect.yml +1 -1
- package/scripts/foundry/plant/agents/mill.yml +1 -1
- package/scripts/foundry/plant/agents/sound-inspect.yml +1 -1
- package/scripts/foundry/plant/agents/sound-mixer.yml +5 -0
- package/scripts/foundry/plant/agents/sound.yml +1 -1
- package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +15 -9
- package/scripts/foundry/plant/motions/registry.json +5 -4
- package/scripts/foundry/plant/skills/blip/SKILL.md +4 -4
- package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +2 -2
- package/scripts/foundry/plant/skills/blip-looker/SKILL.md +30 -0
- package/scripts/foundry/plant/skills/blip-vibe/SKILL.md +30 -0
- package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
- package/scripts/foundry/plant/skills/sound/SKILL.md +3 -1
- package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +1 -1
- package/scripts/foundry/plant/skills/sound-mixer/SKILL.md +28 -0
- package/scripts/foundry/sound-bed.cjs +2 -454
- package/scripts/foundry/sound-mixer.cjs +2 -0
- package/scripts/foundry/sound-rippel.cjs +2 -1434
- package/scripts/foundry/sound-taste.cjs +2 -0
- package/scripts/foundry/sound-voice.cjs +2 -0
- package/scripts/foundry/sound.mjs +23 -81
- package/scripts/node/install-bridges.cjs +39 -4
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/skills/orchestrator/SKILL.md +19 -1
- package/xray/features.json +2 -2
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foundry-plant/0 — mill packages declare plants; foundry does not catalog organs.
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { createRequire } = require("module");
|
|
9
|
+
|
|
10
|
+
const PROTOCOL = "foundry-plant/0";
|
|
11
|
+
const BUILTIN_MILL = { skills: ["mill", "inspect"], agents: ["mill.yml", "inspect.yml"] };
|
|
12
|
+
const PACKAGE_ALIASES = {
|
|
13
|
+
blip: "@0xray/blip",
|
|
14
|
+
sound: "@0xray/blip",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function plantError(message, code) {
|
|
18
|
+
const err = new Error(message);
|
|
19
|
+
err.code = code || "FOUNDRY_PLANT";
|
|
20
|
+
return err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unresolvedPackageError(packageName) {
|
|
24
|
+
const name = typeof packageName === "string" && packageName.trim() ? packageName.trim() : "mill";
|
|
25
|
+
return plantError(
|
|
26
|
+
`foundry-plant: mill package "${name}" is not installed. npm i ${name}`,
|
|
27
|
+
"FOUNDRY_MILL_UNRESOLVED",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isDirectory(p) {
|
|
32
|
+
try {
|
|
33
|
+
return Boolean(p) && fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function listSkillNamesAt(skillsSrc) {
|
|
40
|
+
if (!isDirectory(skillsSrc)) return [];
|
|
41
|
+
const names = [];
|
|
42
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
43
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
44
|
+
if (!fs.existsSync(path.join(skillsSrc, entry.name, "SKILL.md"))) continue;
|
|
45
|
+
names.push(entry.name);
|
|
46
|
+
}
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function listAgentFilesAt(agentsSrc) {
|
|
51
|
+
if (!isDirectory(agentsSrc)) return [];
|
|
52
|
+
return fs
|
|
53
|
+
.readdirSync(agentsSrc, { withFileTypes: true })
|
|
54
|
+
.filter(
|
|
55
|
+
(entry) =>
|
|
56
|
+
entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")),
|
|
57
|
+
)
|
|
58
|
+
.map((entry) => entry.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function existingPlantFiles(plantDir, spec) {
|
|
62
|
+
if (!plantDir || !spec) return { skills: [], agents: [] };
|
|
63
|
+
const haveSkills = new Set(listSkillNamesAt(path.join(plantDir, "skills")));
|
|
64
|
+
const haveAgents = new Set(listAgentFilesAt(path.join(plantDir, "agents")));
|
|
65
|
+
return {
|
|
66
|
+
skills: (spec.skills || []).filter((name) => haveSkills.has(name)),
|
|
67
|
+
agents: (spec.agents || []).filter((file) => haveAgents.has(file)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readFoundryExtra(targetDir) {
|
|
72
|
+
if (!targetDir) return {};
|
|
73
|
+
const candidates = [
|
|
74
|
+
path.join(targetDir, "foundry.json"),
|
|
75
|
+
path.join(targetDir, ".xray", "foundry.json"),
|
|
76
|
+
];
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
if (!fs.existsSync(candidate)) continue;
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
81
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function foundryPlantDir(millPackageRoot) {
|
|
90
|
+
const bases = millPackageRoot ? [millPackageRoot] : [__dirname];
|
|
91
|
+
for (const base of bases) {
|
|
92
|
+
const nested = path.join(base, "scripts", "foundry", "plant");
|
|
93
|
+
if (isDirectory(nested)) return nested;
|
|
94
|
+
const packed = path.join(base, "plant");
|
|
95
|
+
if (isDirectory(packed)) return packed;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isPackageName(name) {
|
|
101
|
+
if (typeof name !== "string" || !name.trim()) return false;
|
|
102
|
+
if (name.startsWith(".") || name.includes("\\")) return false;
|
|
103
|
+
const parts = name.split("/");
|
|
104
|
+
if (parts.includes("..") || parts.includes("")) return false;
|
|
105
|
+
if (name.startsWith("@")) {
|
|
106
|
+
return parts.length === 2 && parts[0].length > 1 && parts[1].length > 0;
|
|
107
|
+
}
|
|
108
|
+
return parts.length === 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parsePlantToken(raw) {
|
|
112
|
+
if (typeof raw !== "string") return null;
|
|
113
|
+
const token = raw.trim();
|
|
114
|
+
if (!token) return null;
|
|
115
|
+
if (token === "mill") {
|
|
116
|
+
return { builtin: true, kind: "mill", plantId: "mill", packageName: null, alias: false };
|
|
117
|
+
}
|
|
118
|
+
if (PACKAGE_ALIASES[token]) {
|
|
119
|
+
return {
|
|
120
|
+
builtin: false,
|
|
121
|
+
kind: token,
|
|
122
|
+
plantId: token,
|
|
123
|
+
packageName: PACKAGE_ALIASES[token],
|
|
124
|
+
alias: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (token.startsWith("@")) {
|
|
128
|
+
const parts = token.split("/");
|
|
129
|
+
if (parts.length < 2 || parts.length > 3) return null;
|
|
130
|
+
if (!parts[0] || parts[0].length < 2 || !parts[1]) return null;
|
|
131
|
+
if (parts.includes("..") || parts.includes(".")) return null;
|
|
132
|
+
const packageName = `${parts[0]}/${parts[1]}`;
|
|
133
|
+
if (!isPackageName(packageName)) return null;
|
|
134
|
+
const plantId = parts.length === 3 && parts[2] ? parts[2] : null;
|
|
135
|
+
return { builtin: false, kind: plantId, plantId, packageName, alias: false };
|
|
136
|
+
}
|
|
137
|
+
if (token.includes("\\") || token.startsWith(".") || token.split("/").includes("..")) return null;
|
|
138
|
+
const slash = token.indexOf("/");
|
|
139
|
+
if (slash === -1) {
|
|
140
|
+
if (!isPackageName(token)) return null;
|
|
141
|
+
return { builtin: false, kind: null, plantId: null, packageName: token, alias: false };
|
|
142
|
+
}
|
|
143
|
+
const packageName = token.slice(0, slash);
|
|
144
|
+
const plantId = token.slice(slash + 1);
|
|
145
|
+
if (!isPackageName(packageName) || !plantId || plantId.includes("/")) return null;
|
|
146
|
+
return { builtin: false, kind: plantId, plantId, packageName, alias: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readPkgName(dir) {
|
|
150
|
+
try {
|
|
151
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
152
|
+
return typeof pkg.name === "string" ? pkg.name : null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function walkNodeModules(fromDir, packageName) {
|
|
159
|
+
let dir = path.resolve(fromDir);
|
|
160
|
+
for (;;) {
|
|
161
|
+
const candidate = path.join(dir, "node_modules", packageName, "package.json");
|
|
162
|
+
if (fs.existsSync(candidate)) return path.dirname(candidate);
|
|
163
|
+
const parent = path.dirname(dir);
|
|
164
|
+
if (parent === dir) break;
|
|
165
|
+
dir = parent;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function packageRootFromResolved(resolved, packageName) {
|
|
171
|
+
let dir = path.resolve(resolved);
|
|
172
|
+
try {
|
|
173
|
+
if (fs.existsSync(dir) && fs.statSync(dir).isFile()) dir = path.dirname(dir);
|
|
174
|
+
} catch {
|
|
175
|
+
dir = path.dirname(dir);
|
|
176
|
+
}
|
|
177
|
+
for (;;) {
|
|
178
|
+
if (readPkgName(dir) === packageName) return dir;
|
|
179
|
+
const parent = path.dirname(dir);
|
|
180
|
+
if (parent === dir) break;
|
|
181
|
+
dir = parent;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function tryRequireResolve(fromDir, packageName) {
|
|
187
|
+
try {
|
|
188
|
+
const req = createRequire(path.join(path.resolve(fromDir), "package.json"));
|
|
189
|
+
return packageRootFromResolved(req.resolve(packageName), packageName);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolvePackageRoot(packageName, dirs) {
|
|
196
|
+
if (!isPackageName(packageName)) return null;
|
|
197
|
+
if (typeof dirs === "string") {
|
|
198
|
+
dirs = { targetDir: dirs, millPackageRoot: dirs, cwd: process.cwd() };
|
|
199
|
+
}
|
|
200
|
+
const d = dirs || {};
|
|
201
|
+
const bases = [d.targetDir, d.millPackageRoot, d.cwd || process.cwd()].filter(
|
|
202
|
+
(base) => typeof base === "string" && base.trim(),
|
|
203
|
+
);
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const base of bases) {
|
|
206
|
+
const abs = path.resolve(base);
|
|
207
|
+
if (seen.has(abs)) continue;
|
|
208
|
+
seen.add(abs);
|
|
209
|
+
if (!fs.existsSync(abs)) continue;
|
|
210
|
+
if (readPkgName(abs) === packageName) return abs;
|
|
211
|
+
const fromNm = walkNodeModules(abs, packageName);
|
|
212
|
+
if (fromNm) return fromNm;
|
|
213
|
+
const fromReq = tryRequireResolve(abs, packageName);
|
|
214
|
+
if (fromReq) return fromReq;
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolvePlantPath(packageRoot, rel) {
|
|
220
|
+
if (typeof rel !== "string" || !rel.trim() || path.isAbsolute(rel)) return null;
|
|
221
|
+
const parts = rel.split(/[\\/]/);
|
|
222
|
+
if (parts.includes("..")) return null;
|
|
223
|
+
const rootAbs = path.resolve(packageRoot);
|
|
224
|
+
const resolved = path.resolve(rootAbs, rel);
|
|
225
|
+
const prefix = rootAbs.endsWith(path.sep) ? rootAbs : `${rootAbs}${path.sep}`;
|
|
226
|
+
if (resolved !== rootAbs && !resolved.startsWith(prefix)) return null;
|
|
227
|
+
return resolved;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readMillOff(spec) {
|
|
231
|
+
if (!spec || typeof spec !== "object") return undefined;
|
|
232
|
+
if (spec.millOff === true) return true;
|
|
233
|
+
if (spec.millOff === false) return false;
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readProtocol(packageRoot) {
|
|
238
|
+
if (!isDirectory(packageRoot)) {
|
|
239
|
+
throw plantError(`foundry-plant: mill package root is missing (${packageRoot || ""})`);
|
|
240
|
+
}
|
|
241
|
+
const pkgPath = path.join(packageRoot, "package.json");
|
|
242
|
+
let pkg;
|
|
243
|
+
try {
|
|
244
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
245
|
+
} catch {
|
|
246
|
+
throw plantError(`foundry-plant: mill package.json is unreadable (${pkgPath})`);
|
|
247
|
+
}
|
|
248
|
+
const packageName = typeof pkg.name === "string" ? pkg.name : null;
|
|
249
|
+
const paths = { skills: "plant/skills", agents: "plant/agents" };
|
|
250
|
+
const mill = pkg.mill;
|
|
251
|
+
if (!mill || typeof mill !== "object" || Array.isArray(mill)) {
|
|
252
|
+
return {
|
|
253
|
+
protocol: PROTOCOL,
|
|
254
|
+
default: null,
|
|
255
|
+
paths,
|
|
256
|
+
plants: {},
|
|
257
|
+
packageName,
|
|
258
|
+
implicit: true,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (typeof mill.protocol === "string" && mill.protocol.trim() && mill.protocol.trim() !== PROTOCOL) {
|
|
262
|
+
throw plantError(
|
|
263
|
+
`foundry-plant: mill package "${packageName || "?"}" declares protocol "${mill.protocol}", expected ${PROTOCOL}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (mill.paths && typeof mill.paths === "object" && !Array.isArray(mill.paths)) {
|
|
267
|
+
if (typeof mill.paths.skills === "string" && mill.paths.skills.trim()) {
|
|
268
|
+
paths.skills = mill.paths.skills.trim();
|
|
269
|
+
}
|
|
270
|
+
if (typeof mill.paths.agents === "string" && mill.paths.agents.trim()) {
|
|
271
|
+
paths.agents = mill.paths.agents.trim();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const plants = {};
|
|
275
|
+
if (mill.plants && typeof mill.plants === "object" && !Array.isArray(mill.plants)) {
|
|
276
|
+
for (const key of Object.keys(mill.plants)) {
|
|
277
|
+
const spec = mill.plants[key];
|
|
278
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) continue;
|
|
279
|
+
const entry = {};
|
|
280
|
+
if (spec.millOff === true) entry.millOff = true;
|
|
281
|
+
if (spec.millOff === false) entry.millOff = false;
|
|
282
|
+
if (Array.isArray(spec.skills)) {
|
|
283
|
+
entry.skills = spec.skills.filter((name) => typeof name === "string" && name.trim());
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(spec.agents)) {
|
|
286
|
+
entry.agents = spec.agents.filter((name) => typeof name === "string" && name.trim());
|
|
287
|
+
}
|
|
288
|
+
plants[key] = entry;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
protocol: PROTOCOL,
|
|
293
|
+
default: typeof mill.default === "string" && mill.default.trim() ? mill.default.trim() : null,
|
|
294
|
+
paths,
|
|
295
|
+
plants,
|
|
296
|
+
packageName,
|
|
297
|
+
implicit: false,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function prefixSelect(plantId, names, isAgent) {
|
|
302
|
+
if (typeof plantId !== "string" || !plantId || !Array.isArray(names)) return [];
|
|
303
|
+
const selected = [];
|
|
304
|
+
for (const name of names) {
|
|
305
|
+
if (typeof name !== "string") continue;
|
|
306
|
+
let base = name;
|
|
307
|
+
if (isAgent) {
|
|
308
|
+
if (!/\.ya?ml$/i.test(name)) continue;
|
|
309
|
+
base = name.replace(/\.ya?ml$/i, "");
|
|
310
|
+
}
|
|
311
|
+
if (base === plantId || base.startsWith(`${plantId}-`)) selected.push(name);
|
|
312
|
+
}
|
|
313
|
+
return selected;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function declaredPlantFiles(packageRoot, plantId, protocol) {
|
|
317
|
+
const proto = protocol || readProtocol(packageRoot);
|
|
318
|
+
const spec = proto.plants[plantId] || {};
|
|
319
|
+
const skillsDir = resolvePlantPath(packageRoot, proto.paths.skills);
|
|
320
|
+
const agentsDir = resolvePlantPath(packageRoot, proto.paths.agents);
|
|
321
|
+
const listedSkills = listSkillNamesAt(skillsDir);
|
|
322
|
+
const listedAgents = listAgentFilesAt(agentsDir);
|
|
323
|
+
const skills =
|
|
324
|
+
Array.isArray(spec.skills) && spec.skills.length > 0
|
|
325
|
+
? spec.skills.filter((name) => typeof name === "string" && name.trim())
|
|
326
|
+
: prefixSelect(plantId, listedSkills, false);
|
|
327
|
+
const agents =
|
|
328
|
+
Array.isArray(spec.agents) && spec.agents.length > 0
|
|
329
|
+
? spec.agents.filter((name) => typeof name === "string" && name.trim())
|
|
330
|
+
: prefixSelect(plantId, listedAgents, true);
|
|
331
|
+
return {
|
|
332
|
+
skills,
|
|
333
|
+
agents,
|
|
334
|
+
millOff: readMillOff(spec),
|
|
335
|
+
skillsDir,
|
|
336
|
+
agentsDir,
|
|
337
|
+
protocol: proto.protocol,
|
|
338
|
+
packageName: proto.packageName,
|
|
339
|
+
plantId,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function selectPlantFiles(packageRoot, plantId, protocol) {
|
|
344
|
+
const declared = declaredPlantFiles(packageRoot, plantId, protocol);
|
|
345
|
+
const haveSkills = new Set(listSkillNamesAt(declared.skillsDir));
|
|
346
|
+
const haveAgents = new Set(listAgentFilesAt(declared.agentsDir));
|
|
347
|
+
return {
|
|
348
|
+
...declared,
|
|
349
|
+
skills: declared.skills.filter((name) => haveSkills.has(name)),
|
|
350
|
+
agents: declared.agents.filter((file) => haveAgents.has(file)),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectPlantTokens(cfg) {
|
|
355
|
+
const raw = cfg && cfg.plant;
|
|
356
|
+
if (Array.isArray(raw)) {
|
|
357
|
+
return { tokens: raw.filter((item) => typeof item === "string"), defaultMill: false };
|
|
358
|
+
}
|
|
359
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
360
|
+
return { tokens: [raw.trim()], defaultMill: false };
|
|
361
|
+
}
|
|
362
|
+
if (cfg && (cfg.soundPlant === true || cfg.blipPlant === true)) {
|
|
363
|
+
const tokens = [];
|
|
364
|
+
if (cfg.soundPlant === true) tokens.push("sound");
|
|
365
|
+
if (cfg.blipPlant === true) tokens.push("blip");
|
|
366
|
+
return { tokens, defaultMill: true };
|
|
367
|
+
}
|
|
368
|
+
return { tokens: [], defaultMill: true };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function builtinMillSeat(millPackageRoot) {
|
|
372
|
+
const plant = foundryPlantDir(millPackageRoot);
|
|
373
|
+
const files = existingPlantFiles(plant, BUILTIN_MILL);
|
|
374
|
+
return {
|
|
375
|
+
kind: "mill",
|
|
376
|
+
builtin: true,
|
|
377
|
+
packageName: null,
|
|
378
|
+
packageRoot: millPackageRoot || __dirname,
|
|
379
|
+
plantId: "mill",
|
|
380
|
+
millOff: false,
|
|
381
|
+
protocol: null,
|
|
382
|
+
skills: files.skills,
|
|
383
|
+
agents: files.agents,
|
|
384
|
+
skillsDir: plant ? path.join(plant, "skills") : null,
|
|
385
|
+
agentsDir: plant ? path.join(plant, "agents") : null,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function includeMill(cfg, defaultMill, seats) {
|
|
390
|
+
if (cfg && cfg.millPlant === false) return false;
|
|
391
|
+
if (defaultMill) return true;
|
|
392
|
+
if (seats.some((seat) => seat.millOff === true)) return false;
|
|
393
|
+
if (seats.some((seat) => seat.millOff === false)) return true;
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function uniqueSeats(seats) {
|
|
398
|
+
const seen = new Set();
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const seat of seats) {
|
|
401
|
+
if (!seat || !seat.kind || seen.has(seat.kind)) continue;
|
|
402
|
+
seen.add(seat.kind);
|
|
403
|
+
out.push(seat);
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function resolvePlantSeats(targetDir, millPackageRoot, extra) {
|
|
409
|
+
const cfg =
|
|
410
|
+
extra && typeof extra === "object" && !Array.isArray(extra) ? extra : readFoundryExtra(targetDir);
|
|
411
|
+
const { tokens, defaultMill } = collectPlantTokens(cfg);
|
|
412
|
+
const dirs = { targetDir, millPackageRoot, cwd: process.cwd() };
|
|
413
|
+
const seats = [];
|
|
414
|
+
|
|
415
|
+
for (const token of tokens) {
|
|
416
|
+
const parsed = parsePlantToken(token);
|
|
417
|
+
if (!parsed) {
|
|
418
|
+
throw plantError(`foundry-plant: invalid foundry.json plant ${JSON.stringify(token)}`);
|
|
419
|
+
}
|
|
420
|
+
if (parsed.builtin) {
|
|
421
|
+
if (!seats.some((seat) => seat.builtin)) seats.push(builtinMillSeat(millPackageRoot));
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const packageRoot = resolvePackageRoot(parsed.packageName, dirs);
|
|
425
|
+
if (!packageRoot) throw unresolvedPackageError(parsed.packageName);
|
|
426
|
+
const protocol = readProtocol(packageRoot);
|
|
427
|
+
const plantId = parsed.plantId || protocol.default;
|
|
428
|
+
if (!plantId) {
|
|
429
|
+
throw plantError(
|
|
430
|
+
`foundry-plant: mill package "${parsed.packageName}" has no default plant id`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const files = selectPlantFiles(packageRoot, plantId, protocol);
|
|
434
|
+
if (!files.skills || files.skills.length === 0) {
|
|
435
|
+
throw plantError(
|
|
436
|
+
`foundry-plant: mill package "${parsed.packageName}" plant "${plantId}" has no skills`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
seats.push({
|
|
440
|
+
kind: plantId,
|
|
441
|
+
builtin: false,
|
|
442
|
+
packageName: protocol.packageName || parsed.packageName,
|
|
443
|
+
packageRoot,
|
|
444
|
+
plantId,
|
|
445
|
+
millOff: files.millOff,
|
|
446
|
+
protocol: protocol.protocol,
|
|
447
|
+
skills: files.skills,
|
|
448
|
+
agents: files.agents,
|
|
449
|
+
skillsDir: files.skillsDir,
|
|
450
|
+
agentsDir: files.agentsDir,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!seats.some((seat) => seat.builtin) && includeMill(cfg, defaultMill, seats)) {
|
|
455
|
+
seats.unshift(builtinMillSeat(millPackageRoot));
|
|
456
|
+
}
|
|
457
|
+
if (cfg.millPlant === false) {
|
|
458
|
+
return uniqueSeats(seats.filter((seat) => !seat.builtin));
|
|
459
|
+
}
|
|
460
|
+
return uniqueSeats(seats);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function plantKindsFromSeats(seats) {
|
|
464
|
+
const kinds = [];
|
|
465
|
+
for (const seat of Array.isArray(seats) ? seats : []) {
|
|
466
|
+
if (seat && seat.kind && !kinds.includes(seat.kind)) kinds.push(seat.kind);
|
|
467
|
+
}
|
|
468
|
+
return kinds;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function plantKindsFromInventory(inventory) {
|
|
472
|
+
if (!inventory || typeof inventory !== "object") return ["mill"];
|
|
473
|
+
if (Array.isArray(inventory.plant) && inventory.plant.length > 0) {
|
|
474
|
+
const kinds = [];
|
|
475
|
+
for (const item of inventory.plant) {
|
|
476
|
+
if (typeof item !== "string") continue;
|
|
477
|
+
const kind = item.trim();
|
|
478
|
+
if (kind && !kinds.includes(kind)) kinds.push(kind);
|
|
479
|
+
}
|
|
480
|
+
if (kinds.length > 0) return kinds;
|
|
481
|
+
}
|
|
482
|
+
if (typeof inventory.plant === "string" && inventory.plant.trim()) {
|
|
483
|
+
return [inventory.plant.trim()];
|
|
484
|
+
}
|
|
485
|
+
const kinds = [];
|
|
486
|
+
if (Array.isArray(inventory.millPlant?.skills) && inventory.millPlant.skills.length > 0) {
|
|
487
|
+
kinds.push("mill");
|
|
488
|
+
}
|
|
489
|
+
if (Array.isArray(inventory.soundPlant?.skills) && inventory.soundPlant.skills.length > 0) {
|
|
490
|
+
kinds.push("sound");
|
|
491
|
+
}
|
|
492
|
+
if (Array.isArray(inventory.blipPlant?.skills) && inventory.blipPlant.skills.length > 0) {
|
|
493
|
+
kinds.push("blip");
|
|
494
|
+
}
|
|
495
|
+
if (inventory.plants && typeof inventory.plants === "object") {
|
|
496
|
+
for (const key of Object.keys(inventory.plants)) {
|
|
497
|
+
const spec = inventory.plants[key];
|
|
498
|
+
if (Array.isArray(spec?.skills) && spec.skills.length > 0 && !kinds.includes(key)) {
|
|
499
|
+
kinds.push(key);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return kinds.length > 0 ? kinds : ["mill"];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function inventoryFilesForKind(inventory, kind) {
|
|
507
|
+
if (kind === "mill") return inventory?.millPlant || { skills: [], agents: [] };
|
|
508
|
+
if (kind === "sound") return inventory?.soundPlant || { skills: [], agents: [] };
|
|
509
|
+
if (kind === "blip") return inventory?.blipPlant || { skills: [], agents: [] };
|
|
510
|
+
return (inventory?.plants && inventory.plants[kind]) || { skills: [], agents: [] };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function parsePlantRef(raw) {
|
|
514
|
+
const parsed = parsePlantToken(raw);
|
|
515
|
+
if (!parsed) {
|
|
516
|
+
throw plantError(`foundry-plant: invalid foundry.json plant ${JSON.stringify(raw)}`);
|
|
517
|
+
}
|
|
518
|
+
return parsed;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function loadPlantRequests(targetDir, millPackageRoot, extra) {
|
|
522
|
+
return resolvePlantSeats(targetDir, millPackageRoot, extra).map((seat) => ({
|
|
523
|
+
plantId: seat.kind,
|
|
524
|
+
builtin: Boolean(seat.builtin),
|
|
525
|
+
packageName: seat.packageName,
|
|
526
|
+
packageRoot: seat.packageRoot,
|
|
527
|
+
millOff: seat.millOff,
|
|
528
|
+
protocol: seat.protocol,
|
|
529
|
+
skills: seat.skills,
|
|
530
|
+
agents: seat.agents,
|
|
531
|
+
skillsDir: seat.skillsDir,
|
|
532
|
+
agentsDir: seat.agentsDir,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function declaredReceiptSeats(inventory, targetDir, millPackageRoot) {
|
|
537
|
+
const kinds = plantKindsFromInventory(inventory);
|
|
538
|
+
const dirs = { targetDir, millPackageRoot, cwd: process.cwd() };
|
|
539
|
+
const seats = [];
|
|
540
|
+
for (const kind of kinds) {
|
|
541
|
+
if (kind === "mill") {
|
|
542
|
+
seats.push({
|
|
543
|
+
kind: "mill",
|
|
544
|
+
builtin: true,
|
|
545
|
+
skills: [...BUILTIN_MILL.skills],
|
|
546
|
+
agents: [...BUILTIN_MILL.agents],
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const packageName =
|
|
551
|
+
(typeof inventory?.millPackage === "string" && inventory.millPackage.trim()) ||
|
|
552
|
+
PACKAGE_ALIASES[kind] ||
|
|
553
|
+
null;
|
|
554
|
+
if (!packageName) throw unresolvedPackageError(kind);
|
|
555
|
+
const packageRoot = resolvePackageRoot(packageName, dirs);
|
|
556
|
+
if (!packageRoot) throw unresolvedPackageError(packageName);
|
|
557
|
+
const protocol = readProtocol(packageRoot);
|
|
558
|
+
const files = declaredPlantFiles(packageRoot, kind, protocol);
|
|
559
|
+
seats.push({
|
|
560
|
+
kind,
|
|
561
|
+
builtin: false,
|
|
562
|
+
packageName: protocol.packageName || packageName,
|
|
563
|
+
packageRoot,
|
|
564
|
+
plantId: kind,
|
|
565
|
+
protocol: protocol.protocol,
|
|
566
|
+
skills: files.skills,
|
|
567
|
+
agents: files.agents,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return seats;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
module.exports = {
|
|
574
|
+
PROTOCOL,
|
|
575
|
+
FOUNDRY_PLANT_PROTOCOL: PROTOCOL,
|
|
576
|
+
BUILTIN_MILL,
|
|
577
|
+
PACKAGE_ALIASES,
|
|
578
|
+
plantError,
|
|
579
|
+
unresolvedPackageError,
|
|
580
|
+
readFoundryExtra,
|
|
581
|
+
foundryPlantDir,
|
|
582
|
+
parsePlantToken,
|
|
583
|
+
parsePlantRef,
|
|
584
|
+
loadPlantRequests,
|
|
585
|
+
resolvePackageRoot,
|
|
586
|
+
readProtocol,
|
|
587
|
+
prefixSelect,
|
|
588
|
+
declaredPlantFiles,
|
|
589
|
+
selectPlantFiles,
|
|
590
|
+
resolvePlantSeats,
|
|
591
|
+
plantKindsFromSeats,
|
|
592
|
+
plantKindsFromInventory,
|
|
593
|
+
inventoryFilesForKind,
|
|
594
|
+
declaredReceiptSeats,
|
|
595
|
+
listSkillNamesAt,
|
|
596
|
+
listAgentFilesAt,
|
|
597
|
+
existingPlantFiles,
|
|
598
|
+
};
|