@floomhq/floom 1.0.64 → 2.0.0
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/index.d.ts +1 -0
- package/dist/index.js +3663 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -25
- package/package.json +37 -45
- package/LICENSE +0 -21
- package/README.md +0 -90
- package/bin/floom.js +0 -2
- package/dist/audit.js +0 -236
- package/dist/cli.js +0 -1313
- package/dist/config.js +0 -85
- package/dist/daemon.js +0 -450
- package/dist/delete.js +0 -55
- package/dist/doctor.js +0 -381
- package/dist/errors.js +0 -71
- package/dist/feedback.js +0 -34
- package/dist/info.js +0 -78
- package/dist/init.js +0 -221
- package/dist/install.js +0 -305
- package/dist/launch.js +0 -110
- package/dist/lib/api.js +0 -142
- package/dist/lib/skill-labels.js +0 -140
- package/dist/library.js +0 -102
- package/dist/list.js +0 -79
- package/dist/login.js +0 -259
- package/dist/mcp.js +0 -20
- package/dist/package.js +0 -507
- package/dist/publish.js +0 -240
- package/dist/push-watch.js +0 -372
- package/dist/scan.js +0 -24
- package/dist/search.js +0 -54
- package/dist/secrets.js +0 -119
- package/dist/setup.js +0 -301
- package/dist/share.js +0 -70
- package/dist/status.js +0 -181
- package/dist/sync-manifest.js +0 -314
- package/dist/sync.js +0 -581
- package/dist/targets.js +0 -49
- package/dist/ui.js +0 -28
- package/dist/whoami.js +0 -64
package/dist/package.js
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, open, readdir, readFile } from "node:fs/promises";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { execFile as execFileCb } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { FloomError } from "./errors.js";
|
|
8
|
-
const ALLOWED_PACKAGE_DIRS = new Set([
|
|
9
|
-
".github",
|
|
10
|
-
"agents",
|
|
11
|
-
"assets",
|
|
12
|
-
"bin",
|
|
13
|
-
"canvas-fonts",
|
|
14
|
-
"checks",
|
|
15
|
-
"claude",
|
|
16
|
-
"codex",
|
|
17
|
-
"contrib",
|
|
18
|
-
"core",
|
|
19
|
-
"cursor",
|
|
20
|
-
"design",
|
|
21
|
-
"docs",
|
|
22
|
-
"evidence",
|
|
23
|
-
"examples",
|
|
24
|
-
"extension",
|
|
25
|
-
"helpers",
|
|
26
|
-
"hosts",
|
|
27
|
-
"kimi",
|
|
28
|
-
"lib",
|
|
29
|
-
"migrations",
|
|
30
|
-
"model-overlays",
|
|
31
|
-
"openclaw",
|
|
32
|
-
"opencode",
|
|
33
|
-
"reference",
|
|
34
|
-
"references",
|
|
35
|
-
"remotion-starter",
|
|
36
|
-
"scripts",
|
|
37
|
-
"sdk",
|
|
38
|
-
"schemas",
|
|
39
|
-
"src",
|
|
40
|
-
"specialists",
|
|
41
|
-
"supabase",
|
|
42
|
-
"templates",
|
|
43
|
-
"test",
|
|
44
|
-
"tests",
|
|
45
|
-
"themes",
|
|
46
|
-
"vendor",
|
|
47
|
-
]);
|
|
48
|
-
const ALLOWED_PACKAGE_ROOT_FILES = new Set(["SKILL.md", ".gitignore"]);
|
|
49
|
-
const ALLOWED_PACKAGE_ROOT_SUPPORT_FILES = new Set([
|
|
50
|
-
".env.example",
|
|
51
|
-
"ACKNOWLEDGEMENTS.md",
|
|
52
|
-
"CHANGELOG.md",
|
|
53
|
-
"LICENSE",
|
|
54
|
-
"LICENSE.md",
|
|
55
|
-
"LICENSE.txt",
|
|
56
|
-
"README.md",
|
|
57
|
-
"AGENTS.md",
|
|
58
|
-
"ARCHITECTURE.md",
|
|
59
|
-
"BROWSER.md",
|
|
60
|
-
"CLAUDE.md",
|
|
61
|
-
"CONTRIBUTING.md",
|
|
62
|
-
"DESIGN.md",
|
|
63
|
-
"ETHOS.md",
|
|
64
|
-
"PLAN-snapshot-dropdown-interactive.md",
|
|
65
|
-
"REMOTION_PROTOCOL.md",
|
|
66
|
-
"SKILL.md.tmpl",
|
|
67
|
-
"TODOS.md",
|
|
68
|
-
"TODOS-format.md",
|
|
69
|
-
"USING_GBRAIN_WITH_GSTACK.md",
|
|
70
|
-
"VERSION",
|
|
71
|
-
".gitlab-ci.yml",
|
|
72
|
-
"actionlint.yaml",
|
|
73
|
-
"bun.lock",
|
|
74
|
-
"checklist.md",
|
|
75
|
-
"conductor.json",
|
|
76
|
-
"design-checklist.md",
|
|
77
|
-
"dx-hall-of-fame.md",
|
|
78
|
-
"editing.md",
|
|
79
|
-
"forms.md",
|
|
80
|
-
"instructions.md",
|
|
81
|
-
"nanobanana.py",
|
|
82
|
-
"package.json",
|
|
83
|
-
"plan.md",
|
|
84
|
-
"pptxgenjs.md",
|
|
85
|
-
"reference.md",
|
|
86
|
-
"requirements.txt",
|
|
87
|
-
"greptile-triage.md",
|
|
88
|
-
"setup",
|
|
89
|
-
"skill.md",
|
|
90
|
-
"slop-scan.config.json",
|
|
91
|
-
"style_guidelines.md",
|
|
92
|
-
"theme-showcase.pdf",
|
|
93
|
-
]);
|
|
94
|
-
const GENERATED_PACKAGE_DIRS = new Set([
|
|
95
|
-
".agents",
|
|
96
|
-
".cache",
|
|
97
|
-
".cursor",
|
|
98
|
-
".factory",
|
|
99
|
-
".gbrain",
|
|
100
|
-
".git",
|
|
101
|
-
".hermes",
|
|
102
|
-
".kiro",
|
|
103
|
-
".next",
|
|
104
|
-
".openclaw",
|
|
105
|
-
".opencode",
|
|
106
|
-
".pytest_cache",
|
|
107
|
-
".slate",
|
|
108
|
-
"__pycache__",
|
|
109
|
-
"build",
|
|
110
|
-
"coverage",
|
|
111
|
-
"dist",
|
|
112
|
-
"fixtures",
|
|
113
|
-
"node_modules",
|
|
114
|
-
"out",
|
|
115
|
-
]);
|
|
116
|
-
const GENERATED_PACKAGE_FILES = new Set([".DS_Store"]);
|
|
117
|
-
const GENERATED_PACKAGE_FILE_SUFFIXES = [".icns", ".log", ".mov", ".mp3", ".mp4", ".pyc", ".pyo", ".wav", ".webm"];
|
|
118
|
-
const PACKAGE_FILE_LIMIT = 100;
|
|
119
|
-
const PACKAGE_TOTAL_BYTES_LIMIT = 8_000_000;
|
|
120
|
-
const PACKAGE_FILE_BYTES_LIMIT = 500_000;
|
|
121
|
-
const PACKAGE_SEGMENT_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
122
|
-
const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
123
|
-
const execFile = promisify(execFileCb);
|
|
124
|
-
export function sha256Bytes(input) {
|
|
125
|
-
return createHash("sha256").update(input).digest("hex");
|
|
126
|
-
}
|
|
127
|
-
export async function readSkillPackage(inputPath) {
|
|
128
|
-
const resolved = resolve(process.cwd(), inputPath);
|
|
129
|
-
let stat;
|
|
130
|
-
try {
|
|
131
|
-
stat = await lstat(resolved);
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
if (err.code === "ENOENT") {
|
|
135
|
-
throw new FloomError(`File not found: ${inputPath}`);
|
|
136
|
-
}
|
|
137
|
-
throw new FloomError(`Couldn't read ${inputPath}: ${err.message}`);
|
|
138
|
-
}
|
|
139
|
-
if (stat.isSymbolicLink()) {
|
|
140
|
-
throw new FloomError(`Package path is a symbolic link: ${inputPath}`);
|
|
141
|
-
}
|
|
142
|
-
if (stat.isFile()) {
|
|
143
|
-
const body = await readSkillFile(resolved, inputPath);
|
|
144
|
-
return {
|
|
145
|
-
rootPath: dirname(resolved),
|
|
146
|
-
skillPath: resolved,
|
|
147
|
-
skillBody: body,
|
|
148
|
-
packageFiles: [],
|
|
149
|
-
originalFilename: basename(resolved),
|
|
150
|
-
isFolder: false,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
if (!stat.isDirectory()) {
|
|
154
|
-
throw new FloomError(`That's not a file or skill folder: ${inputPath}`);
|
|
155
|
-
}
|
|
156
|
-
const skillPath = join(resolved, "SKILL.md");
|
|
157
|
-
const skillBody = await readSkillFile(skillPath, join(inputPath, "SKILL.md"));
|
|
158
|
-
const packageFiles = await collectPackageFiles(resolved);
|
|
159
|
-
return {
|
|
160
|
-
rootPath: resolved,
|
|
161
|
-
skillPath,
|
|
162
|
-
skillBody,
|
|
163
|
-
packageFiles,
|
|
164
|
-
originalFilename: `${basename(resolved)}/SKILL.md`,
|
|
165
|
-
isFolder: true,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
async function readSkillFile(path, label) {
|
|
169
|
-
let body;
|
|
170
|
-
try {
|
|
171
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
172
|
-
try {
|
|
173
|
-
const stat = await handle.stat();
|
|
174
|
-
if (!stat.isFile())
|
|
175
|
-
throw new FloomError(`That's a directory, not a file: ${label}`);
|
|
176
|
-
if (stat.size > PACKAGE_FILE_BYTES_LIMIT) {
|
|
177
|
-
throw new FloomError(`File too large: ${label}`, "Each package file is capped at 500KB.");
|
|
178
|
-
}
|
|
179
|
-
body = await handle.readFile("utf8");
|
|
180
|
-
}
|
|
181
|
-
finally {
|
|
182
|
-
await handle.close();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
const code = err.code;
|
|
187
|
-
if (code === "ENOENT")
|
|
188
|
-
throw new FloomError(`File not found: ${label}`);
|
|
189
|
-
if (code === "ELOOP")
|
|
190
|
-
throw new FloomError(`Package file is a symbolic link: ${label}`);
|
|
191
|
-
if (err instanceof FloomError)
|
|
192
|
-
throw err;
|
|
193
|
-
throw new FloomError(`Couldn't read ${label}: ${err.message}`);
|
|
194
|
-
}
|
|
195
|
-
if (!body.trim()) {
|
|
196
|
-
throw new FloomError(`File is empty: ${label}`, "Add skill instructions before scanning or publishing.");
|
|
197
|
-
}
|
|
198
|
-
return body;
|
|
199
|
-
}
|
|
200
|
-
async function collectPackageFiles(root) {
|
|
201
|
-
const files = [];
|
|
202
|
-
let totalBytes = Buffer.byteLength(await readFile(join(root, "SKILL.md")));
|
|
203
|
-
const isIgnored = gitIgnoreChecker(root);
|
|
204
|
-
const rootEntries = await readdir(root, { withFileTypes: true });
|
|
205
|
-
for (const entry of rootEntries) {
|
|
206
|
-
if (ALLOWED_PACKAGE_ROOT_FILES.has(entry.name) || ALLOWED_PACKAGE_DIRS.has(entry.name))
|
|
207
|
-
continue;
|
|
208
|
-
if (ALLOWED_PACKAGE_ROOT_SUPPORT_FILES.has(entry.name)) {
|
|
209
|
-
if (entry.isSymbolicLink())
|
|
210
|
-
continue;
|
|
211
|
-
if (!entry.isFile())
|
|
212
|
-
throw new FloomError(`Package path is not a regular file: ${entry.name}`);
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (isGeneratedPackageEntry(entry.name, entry.isDirectory()))
|
|
216
|
-
continue;
|
|
217
|
-
if (await isIgnored(entry.name))
|
|
218
|
-
continue;
|
|
219
|
-
if (entry.isDirectory() && await hasNestedSkillDir(join(root, entry.name)))
|
|
220
|
-
continue;
|
|
221
|
-
if (entry.isSymbolicLink())
|
|
222
|
-
continue;
|
|
223
|
-
const hint = "Move supporting files under references/, examples/, scripts/, or assets/.";
|
|
224
|
-
throw new FloomError(`Unsupported root package entry: ${entry.name}`, hint);
|
|
225
|
-
}
|
|
226
|
-
for (const entry of rootEntries) {
|
|
227
|
-
if (!ALLOWED_PACKAGE_ROOT_SUPPORT_FILES.has(entry.name))
|
|
228
|
-
continue;
|
|
229
|
-
if (entry.isSymbolicLink())
|
|
230
|
-
continue;
|
|
231
|
-
const fullPath = join(root, entry.name);
|
|
232
|
-
if (await isIgnored(entry.name))
|
|
233
|
-
continue;
|
|
234
|
-
await collectFile(fullPath, entry.name, files, (size) => {
|
|
235
|
-
totalBytes += size;
|
|
236
|
-
if (totalBytes > PACKAGE_TOTAL_BYTES_LIMIT) {
|
|
237
|
-
throw new FloomError("Skill package is too large.", "A skill package is capped at 8MB total.");
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
for (const dirName of ALLOWED_PACKAGE_DIRS) {
|
|
242
|
-
const dirPath = join(root, dirName);
|
|
243
|
-
let stat;
|
|
244
|
-
try {
|
|
245
|
-
stat = await lstat(dirPath);
|
|
246
|
-
}
|
|
247
|
-
catch (err) {
|
|
248
|
-
if (err.code === "ENOENT")
|
|
249
|
-
continue;
|
|
250
|
-
throw err;
|
|
251
|
-
}
|
|
252
|
-
if (await isIgnored(dirName))
|
|
253
|
-
continue;
|
|
254
|
-
if (stat.isSymbolicLink())
|
|
255
|
-
continue;
|
|
256
|
-
if (!stat.isDirectory())
|
|
257
|
-
throw new FloomError(`Package entry must be a directory: ${dirName}`);
|
|
258
|
-
await collectDir(root, dirPath, files, isIgnored, (size) => {
|
|
259
|
-
totalBytes += size;
|
|
260
|
-
if (totalBytes > PACKAGE_TOTAL_BYTES_LIMIT) {
|
|
261
|
-
throw new FloomError("Skill package is too large.", "A skill package is capped at 8MB total.");
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
266
|
-
}
|
|
267
|
-
async function collectDir(root, dir, files, isIgnored, addBytes) {
|
|
268
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
269
|
-
for (const entry of entries) {
|
|
270
|
-
if (isGeneratedPackageEntry(entry.name, entry.isDirectory()))
|
|
271
|
-
continue;
|
|
272
|
-
if (!PACKAGE_SEGMENT_RE.test(entry.name)) {
|
|
273
|
-
throw new FloomError(`Invalid package path segment: ${entry.name}`);
|
|
274
|
-
}
|
|
275
|
-
const fullPath = join(dir, entry.name);
|
|
276
|
-
const rel = packageRelativePath(root, fullPath);
|
|
277
|
-
if (await isIgnored(rel))
|
|
278
|
-
continue;
|
|
279
|
-
if (entry.isSymbolicLink())
|
|
280
|
-
continue;
|
|
281
|
-
if (entry.isDirectory()) {
|
|
282
|
-
await collectDir(root, fullPath, files, isIgnored, addBytes);
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (!entry.isFile())
|
|
286
|
-
throw new FloomError(`Package path is not a regular file: ${rel}`);
|
|
287
|
-
await collectFile(fullPath, rel, files, addBytes);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
async function hasNestedSkillDir(path) {
|
|
291
|
-
return hasNestedSkillDirInner(path, 0);
|
|
292
|
-
}
|
|
293
|
-
async function hasNestedSkillDirInner(path, depth) {
|
|
294
|
-
if (depth > 8)
|
|
295
|
-
return false;
|
|
296
|
-
try {
|
|
297
|
-
const stat = await lstat(join(path, "SKILL.md"));
|
|
298
|
-
if (stat.isFile())
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
catch (err) {
|
|
302
|
-
if (err.code !== "ENOENT")
|
|
303
|
-
throw err;
|
|
304
|
-
}
|
|
305
|
-
let entries;
|
|
306
|
-
try {
|
|
307
|
-
entries = await readdir(path, { withFileTypes: true });
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
if (err.code === "ENOENT")
|
|
311
|
-
return false;
|
|
312
|
-
throw err;
|
|
313
|
-
}
|
|
314
|
-
for (const entry of entries) {
|
|
315
|
-
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
316
|
-
continue;
|
|
317
|
-
if (isGeneratedPackageEntry(entry.name, true))
|
|
318
|
-
continue;
|
|
319
|
-
if (await hasNestedSkillDirInner(join(path, entry.name), depth + 1))
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
async function collectFile(fullPath, rel, files, addBytes) {
|
|
325
|
-
const normalized = rel.split(sep).join("/");
|
|
326
|
-
validatePackageRelativePath(normalized);
|
|
327
|
-
if (files.length >= PACKAGE_FILE_LIMIT) {
|
|
328
|
-
throw new FloomError("Skill package has too many files.", "A skill package is capped at 100 supporting files.");
|
|
329
|
-
}
|
|
330
|
-
const stat = await lstat(fullPath);
|
|
331
|
-
if (stat.isSymbolicLink())
|
|
332
|
-
throw new FloomError(`Package path is a symbolic link: ${normalized}`);
|
|
333
|
-
if (!stat.isFile())
|
|
334
|
-
throw new FloomError(`Package path is not a regular file: ${normalized}`);
|
|
335
|
-
if (stat.size > PACKAGE_FILE_BYTES_LIMIT) {
|
|
336
|
-
throw new FloomError(`File too large: ${normalized}`, "Each package file is capped at 500KB.");
|
|
337
|
-
}
|
|
338
|
-
addBytes(stat.size);
|
|
339
|
-
const bytes = await readFile(fullPath);
|
|
340
|
-
files.push({
|
|
341
|
-
path: normalized,
|
|
342
|
-
content_base64: bytes.toString("base64"),
|
|
343
|
-
encoding: "base64",
|
|
344
|
-
size_bytes: bytes.length,
|
|
345
|
-
sha256: sha256Bytes(bytes),
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
function isGeneratedPackageEntry(name, isDirectory) {
|
|
349
|
-
if (isDirectory && GENERATED_PACKAGE_DIRS.has(name))
|
|
350
|
-
return true;
|
|
351
|
-
if (GENERATED_PACKAGE_FILES.has(name))
|
|
352
|
-
return true;
|
|
353
|
-
return GENERATED_PACKAGE_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
354
|
-
}
|
|
355
|
-
function gitIgnoreChecker(root) {
|
|
356
|
-
const cache = new Map();
|
|
357
|
-
let localRules = null;
|
|
358
|
-
return async (rel) => {
|
|
359
|
-
const cached = cache.get(rel);
|
|
360
|
-
if (cached !== undefined)
|
|
361
|
-
return cached;
|
|
362
|
-
let ignored = false;
|
|
363
|
-
try {
|
|
364
|
-
await execFile("git", ["-C", root, "check-ignore", "--quiet", "--", rel]);
|
|
365
|
-
ignored = true;
|
|
366
|
-
}
|
|
367
|
-
catch (err) {
|
|
368
|
-
const code = err.code;
|
|
369
|
-
if (code !== 1 && code !== 128 && code !== "ENOENT") {
|
|
370
|
-
ignored = false;
|
|
371
|
-
}
|
|
372
|
-
if (code === 128 || code === "ENOENT") {
|
|
373
|
-
localRules ??= readLocalGitignore(root);
|
|
374
|
-
ignored = matchesLocalGitignore(rel, await localRules);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
cache.set(rel, ignored);
|
|
378
|
-
return ignored;
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
async function readLocalGitignore(root) {
|
|
382
|
-
try {
|
|
383
|
-
const body = await readFile(join(root, ".gitignore"), "utf8");
|
|
384
|
-
return body
|
|
385
|
-
.split(/\r?\n/)
|
|
386
|
-
.map((line) => line.trim())
|
|
387
|
-
.filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("!"));
|
|
388
|
-
}
|
|
389
|
-
catch {
|
|
390
|
-
return [];
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
function matchesLocalGitignore(rel, rules) {
|
|
394
|
-
const normalized = rel.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
395
|
-
for (const rawRule of rules) {
|
|
396
|
-
const rule = rawRule.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
397
|
-
if (!rule)
|
|
398
|
-
continue;
|
|
399
|
-
if (rule.endsWith("/")) {
|
|
400
|
-
const prefix = rule.replace(/\/+$/, "");
|
|
401
|
-
if (normalized === prefix || normalized.startsWith(`${prefix}/`))
|
|
402
|
-
return true;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (normalized === rule || normalized.endsWith(`/${rule}`))
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
function packageRelativePath(root, path) {
|
|
411
|
-
const rel = relative(resolve(root), resolve(path));
|
|
412
|
-
if (!rel || rel === "." || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
413
|
-
throw new FloomError("Invalid package path.");
|
|
414
|
-
}
|
|
415
|
-
const normalized = rel.split(sep).join("/");
|
|
416
|
-
validatePackageRelativePath(normalized);
|
|
417
|
-
return normalized;
|
|
418
|
-
}
|
|
419
|
-
export function validatePackageRelativePath(path) {
|
|
420
|
-
if (!path || isAbsolute(path) || path.includes("\\") || path.length > 512) {
|
|
421
|
-
throw new FloomError(`Invalid package file path: ${path}`);
|
|
422
|
-
}
|
|
423
|
-
const segments = path.split("/");
|
|
424
|
-
if (segments.length === 1) {
|
|
425
|
-
if (ALLOWED_PACKAGE_ROOT_SUPPORT_FILES.has(path) && PACKAGE_SEGMENT_RE.test(path))
|
|
426
|
-
return;
|
|
427
|
-
throw new FloomError(`Invalid package file path: ${path}`, "Package root files must be approved support files.");
|
|
428
|
-
}
|
|
429
|
-
if (!ALLOWED_PACKAGE_DIRS.has(segments[0] ?? "")) {
|
|
430
|
-
throw new FloomError(`Invalid package file path: ${path}`, "Package files must be under an approved support directory.");
|
|
431
|
-
}
|
|
432
|
-
if (segments.some((segment) => segment === "." || segment === ".." || !PACKAGE_SEGMENT_RE.test(segment))) {
|
|
433
|
-
throw new FloomError(`Invalid package file path: ${path}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
export function normalizeRemotePackageFiles(input) {
|
|
437
|
-
if (input === undefined || input === null)
|
|
438
|
-
return [];
|
|
439
|
-
if (!Array.isArray(input))
|
|
440
|
-
throw new FloomError("Invalid skill package response.");
|
|
441
|
-
if (input.length > PACKAGE_FILE_LIMIT) {
|
|
442
|
-
throw new FloomError("Skill package response has too many files.");
|
|
443
|
-
}
|
|
444
|
-
let totalBytes = 0;
|
|
445
|
-
const seenPaths = new Set();
|
|
446
|
-
return input.map((raw) => {
|
|
447
|
-
if (!raw || typeof raw !== "object")
|
|
448
|
-
throw new FloomError("Invalid skill package response.");
|
|
449
|
-
const file = raw;
|
|
450
|
-
if (typeof file.path !== "string")
|
|
451
|
-
throw new FloomError("Invalid skill package response.");
|
|
452
|
-
validatePackageRelativePath(file.path);
|
|
453
|
-
const pathKey = file.path.toLowerCase();
|
|
454
|
-
if (seenPaths.has(pathKey))
|
|
455
|
-
throw new FloomError(`Duplicate package file: ${file.path}`);
|
|
456
|
-
seenPaths.add(pathKey);
|
|
457
|
-
let bytes;
|
|
458
|
-
if (typeof file.content_base64 === "string") {
|
|
459
|
-
if (file.encoding !== undefined && file.encoding !== "base64") {
|
|
460
|
-
throw new FloomError(`Invalid package file encoding: ${file.path}`);
|
|
461
|
-
}
|
|
462
|
-
if (file.content_base64.length % 4 !== 0 || !BASE64_RE.test(file.content_base64)) {
|
|
463
|
-
throw new FloomError(`Invalid package file base64: ${file.path}`);
|
|
464
|
-
}
|
|
465
|
-
bytes = Buffer.from(file.content_base64, "base64");
|
|
466
|
-
}
|
|
467
|
-
else if (typeof file.content === "string") {
|
|
468
|
-
bytes = Buffer.from(file.content, "utf8");
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
throw new FloomError("Invalid skill package response.");
|
|
472
|
-
}
|
|
473
|
-
if (bytes.length > PACKAGE_FILE_BYTES_LIMIT) {
|
|
474
|
-
throw new FloomError(`Package file is too large: ${file.path}`);
|
|
475
|
-
}
|
|
476
|
-
if (typeof file.size_bytes === "number" && file.size_bytes !== bytes.length) {
|
|
477
|
-
throw new FloomError(`Package file size mismatch: ${file.path}`);
|
|
478
|
-
}
|
|
479
|
-
totalBytes += bytes.length;
|
|
480
|
-
if (totalBytes > PACKAGE_TOTAL_BYTES_LIMIT) {
|
|
481
|
-
throw new FloomError("Skill package response is too large.");
|
|
482
|
-
}
|
|
483
|
-
const hash = sha256Bytes(bytes);
|
|
484
|
-
if (typeof file.content_base64 === "string" && typeof file.sha256 !== "string") {
|
|
485
|
-
throw new FloomError(`Package file missing sha256: ${file.path}`);
|
|
486
|
-
}
|
|
487
|
-
if (typeof file.sha256 === "string" && file.sha256 !== hash) {
|
|
488
|
-
throw new FloomError(`Package file hash mismatch: ${file.path}`);
|
|
489
|
-
}
|
|
490
|
-
return { path: file.path, bytes, sha256: hash };
|
|
491
|
-
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
492
|
-
}
|
|
493
|
-
export function packageHash(skillBody, files) {
|
|
494
|
-
const hash = createHash("sha256");
|
|
495
|
-
hash.update("SKILL.md\0");
|
|
496
|
-
hash.update(skillBody);
|
|
497
|
-
for (const file of files) {
|
|
498
|
-
hash.update("\0");
|
|
499
|
-
hash.update(file.path);
|
|
500
|
-
hash.update("\0");
|
|
501
|
-
if ("bytes" in file)
|
|
502
|
-
hash.update(file.bytes);
|
|
503
|
-
else
|
|
504
|
-
hash.update(Buffer.from(file.content_base64, "base64"));
|
|
505
|
-
}
|
|
506
|
-
return hash.digest("hex");
|
|
507
|
-
}
|