@floomhq/skills 0.2.12 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +452 -99
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/index.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -111,13 +111,13 @@ var require_bcrypt = __commonJS({
|
|
|
111
111
|
throw Error("Illegal callback: " + typeof callback);
|
|
112
112
|
_async(callback);
|
|
113
113
|
} else
|
|
114
|
-
return new Promise(function(
|
|
114
|
+
return new Promise(function(resolve5, reject) {
|
|
115
115
|
_async(function(err, res) {
|
|
116
116
|
if (err) {
|
|
117
117
|
reject(err);
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
resolve5(res);
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
};
|
|
@@ -146,13 +146,13 @@ var require_bcrypt = __commonJS({
|
|
|
146
146
|
throw Error("Illegal callback: " + typeof callback);
|
|
147
147
|
_async(callback);
|
|
148
148
|
} else
|
|
149
|
-
return new Promise(function(
|
|
149
|
+
return new Promise(function(resolve5, reject) {
|
|
150
150
|
_async(function(err, res) {
|
|
151
151
|
if (err) {
|
|
152
152
|
reject(err);
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
resolve5(res);
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
};
|
|
@@ -197,13 +197,13 @@ var require_bcrypt = __commonJS({
|
|
|
197
197
|
throw Error("Illegal callback: " + typeof callback);
|
|
198
198
|
_async(callback);
|
|
199
199
|
} else
|
|
200
|
-
return new Promise(function(
|
|
200
|
+
return new Promise(function(resolve5, reject) {
|
|
201
201
|
_async(function(err, res) {
|
|
202
202
|
if (err) {
|
|
203
203
|
reject(err);
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
resolve5(res);
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
209
|
};
|
|
@@ -1908,9 +1908,9 @@ function parseManifest(raw) {
|
|
|
1908
1908
|
}
|
|
1909
1909
|
async function readManifest(skillDir) {
|
|
1910
1910
|
const { readFile: readFile11 } = await import("node:fs/promises");
|
|
1911
|
-
const { join:
|
|
1911
|
+
const { join: join16 } = await import("node:path");
|
|
1912
1912
|
try {
|
|
1913
|
-
const raw = await readFile11(
|
|
1913
|
+
const raw = await readFile11(join16(skillDir, "skill.json"), "utf8");
|
|
1914
1914
|
let parsed;
|
|
1915
1915
|
try {
|
|
1916
1916
|
parsed = JSON.parse(raw);
|
|
@@ -2012,8 +2012,9 @@ import { promisify } from "node:util";
|
|
|
2012
2012
|
var scrypt = promisify(scryptCb);
|
|
2013
2013
|
|
|
2014
2014
|
// ../shared/src/install-targets.ts
|
|
2015
|
-
import {
|
|
2016
|
-
import {
|
|
2015
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2016
|
+
import { homedir, tmpdir } from "node:os";
|
|
2017
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
2017
2018
|
var INSTALL_TARGETS = [
|
|
2018
2019
|
"generic",
|
|
2019
2020
|
"all",
|
|
@@ -2047,10 +2048,36 @@ var TARGET_ENV_DIRS = {
|
|
|
2047
2048
|
opencode: ["FLOOM_SKILLS_DIR", "OPENCODE_SKILLS_DIR", "AGENTS_SKILLS_DIR"],
|
|
2048
2049
|
kimi: ["FLOOM_SKILLS_DIR", "KIMI_SKILLS_DIR", "AGENTS_SKILLS_DIR"]
|
|
2049
2050
|
};
|
|
2051
|
+
var SYSTEM_ROOTS = /* @__PURE__ */ new Set(["/", "/bin", "/etc", "/proc", "/sbin", "/sys", "/usr", "/var"]);
|
|
2052
|
+
function resolveThroughExistingParent(path) {
|
|
2053
|
+
let current = path;
|
|
2054
|
+
while (!existsSync(current)) {
|
|
2055
|
+
const parent = dirname(current);
|
|
2056
|
+
if (parent === current) return path;
|
|
2057
|
+
current = parent;
|
|
2058
|
+
}
|
|
2059
|
+
const realParent = realpathSync.native(current);
|
|
2060
|
+
return resolve(realParent, relative(current, path));
|
|
2061
|
+
}
|
|
2062
|
+
function validateEnvInstallDir(envVar, rawValue) {
|
|
2063
|
+
const trimmed = rawValue.trim();
|
|
2064
|
+
if (!trimmed || trimmed.includes("\0")) {
|
|
2065
|
+
throw new Error(`${envVar} must be a non-empty filesystem path`);
|
|
2066
|
+
}
|
|
2067
|
+
const resolved = resolve(trimmed);
|
|
2068
|
+
const resolvedHome = resolve(homedir());
|
|
2069
|
+
const realTarget = resolveThroughExistingParent(resolved);
|
|
2070
|
+
if (SYSTEM_ROOTS.has(realTarget) || !isWithin(realTarget, resolvedHome)) {
|
|
2071
|
+
throw new Error(
|
|
2072
|
+
`${envVar} points outside your home directory (${realTarget}). Set it to a path under ${resolvedHome} or use --target without an env override.`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
return realTarget;
|
|
2076
|
+
}
|
|
2050
2077
|
function envDirForTarget(target) {
|
|
2051
2078
|
for (const key of TARGET_ENV_DIRS[target]) {
|
|
2052
2079
|
const value = process.env[key]?.trim();
|
|
2053
|
-
if (value) return value;
|
|
2080
|
+
if (value) return validateEnvInstallDir(key, value);
|
|
2054
2081
|
}
|
|
2055
2082
|
return null;
|
|
2056
2083
|
}
|
|
@@ -2073,12 +2100,35 @@ function presetDir(target, opts) {
|
|
|
2073
2100
|
return join(root, ".agents", "skills");
|
|
2074
2101
|
}
|
|
2075
2102
|
}
|
|
2103
|
+
function isWithin(child, parent) {
|
|
2104
|
+
const rel = relative(parent, child);
|
|
2105
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
2106
|
+
}
|
|
2107
|
+
function validateExplicitInstallDir(dir, cwd = process.cwd()) {
|
|
2108
|
+
const trimmed = dir.trim();
|
|
2109
|
+
if (!trimmed || trimmed.includes("\0")) {
|
|
2110
|
+
throw new Error("--to must be a non-empty filesystem path");
|
|
2111
|
+
}
|
|
2112
|
+
const absolute = resolve(cwd, trimmed);
|
|
2113
|
+
if (!isAbsolute(trimmed)) {
|
|
2114
|
+
const cwdRoot = resolve(cwd);
|
|
2115
|
+
if (!isWithin(absolute, cwdRoot)) {
|
|
2116
|
+
throw new Error("--to must stay inside the current project when using a relative path");
|
|
2117
|
+
}
|
|
2118
|
+
return normalize(trimmed);
|
|
2119
|
+
}
|
|
2120
|
+
const allowedAbsoluteRoots = [homedir(), tmpdir()].map((root) => resolve(root));
|
|
2121
|
+
if (!allowedAbsoluteRoots.some((root) => isWithin(absolute, root))) {
|
|
2122
|
+
throw new Error("--to absolute paths must be inside your home directory or temporary directory");
|
|
2123
|
+
}
|
|
2124
|
+
return absolute;
|
|
2125
|
+
}
|
|
2076
2126
|
function resolveInstallDir(args) {
|
|
2077
2127
|
const target = args.target ?? "generic";
|
|
2078
2128
|
if (args.to) {
|
|
2079
2129
|
return {
|
|
2080
2130
|
target,
|
|
2081
|
-
dir: args.to,
|
|
2131
|
+
dir: validateExplicitInstallDir(args.to, args.cwd),
|
|
2082
2132
|
origin: "explicit",
|
|
2083
2133
|
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2084
2134
|
};
|
|
@@ -2099,6 +2149,13 @@ function resolveInstallDir(args) {
|
|
|
2099
2149
|
compatibleAgents: COMPATIBLE_AGENTS[target]
|
|
2100
2150
|
};
|
|
2101
2151
|
}
|
|
2152
|
+
function normalizeInstallParentDir(parentDir, skillSlug) {
|
|
2153
|
+
const normalized = normalize(parentDir);
|
|
2154
|
+
if (basename(normalized) === skillSlug) {
|
|
2155
|
+
return join(normalized, "..");
|
|
2156
|
+
}
|
|
2157
|
+
return normalized;
|
|
2158
|
+
}
|
|
2102
2159
|
|
|
2103
2160
|
// ../shared/src/security-scan.ts
|
|
2104
2161
|
import { readFile } from "node:fs/promises";
|
|
@@ -2222,7 +2279,7 @@ async function scanSkillBundleFiles(files) {
|
|
|
2222
2279
|
|
|
2223
2280
|
// ../shared/src/skill-package.ts
|
|
2224
2281
|
import { readFile as readFile2, readdir, stat, lstat, mkdir } from "node:fs/promises";
|
|
2225
|
-
import { join as join2, relative, sep, posix } from "node:path";
|
|
2282
|
+
import { join as join2, relative as relative2, sep, posix } from "node:path";
|
|
2226
2283
|
import { createHash as createHash2 } from "node:crypto";
|
|
2227
2284
|
var LIMITS = {
|
|
2228
2285
|
maxBundleBytes: 10 * 1024 * 1024,
|
|
@@ -2278,7 +2335,7 @@ async function walk(rootDir, ignore, acc, current) {
|
|
|
2278
2335
|
const entries = await readdir(current, { withFileTypes: true });
|
|
2279
2336
|
for (const entry of entries) {
|
|
2280
2337
|
const abs = join2(current, entry.name);
|
|
2281
|
-
const rel =
|
|
2338
|
+
const rel = relative2(rootDir, abs).split(sep).join(posix.sep);
|
|
2282
2339
|
if (entry.isSymbolicLink()) {
|
|
2283
2340
|
throw new Error(`Symlinks are not allowed: ${rel}`);
|
|
2284
2341
|
}
|
|
@@ -2347,7 +2404,7 @@ async function extractBundle(buf, destDir) {
|
|
|
2347
2404
|
const tar = await import("tar");
|
|
2348
2405
|
const { Readable } = await import("node:stream");
|
|
2349
2406
|
await mkdir(destDir, { recursive: true });
|
|
2350
|
-
await new Promise((
|
|
2407
|
+
await new Promise((resolve5, reject) => {
|
|
2351
2408
|
const extractStream = tar.extract({
|
|
2352
2409
|
cwd: destDir,
|
|
2353
2410
|
strict: true,
|
|
@@ -2361,7 +2418,7 @@ async function extractBundle(buf, destDir) {
|
|
|
2361
2418
|
if (msg.toLowerCase().includes("symlink")) reject(new Error("Bundle contains symlinks"));
|
|
2362
2419
|
}
|
|
2363
2420
|
});
|
|
2364
|
-
Readable.from(buf).pipe(extractStream).on("finish", () =>
|
|
2421
|
+
Readable.from(buf).pipe(extractStream).on("finish", () => resolve5()).on("error", reject);
|
|
2365
2422
|
});
|
|
2366
2423
|
}
|
|
2367
2424
|
function verifyBundleHash(buf, expected) {
|
|
@@ -2407,7 +2464,11 @@ async function readRawAuth() {
|
|
|
2407
2464
|
const raw = await readFile3(AUTH_FILE, "utf8");
|
|
2408
2465
|
return JSON.parse(raw);
|
|
2409
2466
|
} catch (e) {
|
|
2410
|
-
|
|
2467
|
+
const code = e.code;
|
|
2468
|
+
if (code === "ENOENT") return null;
|
|
2469
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
2470
|
+
throw new FloomError("AUTH_REQUIRED", "Cannot read ~/.floom/auth.json due to file permissions. Fix permissions or run: floom login");
|
|
2471
|
+
}
|
|
2411
2472
|
if (e instanceof SyntaxError) {
|
|
2412
2473
|
throw new FloomError("AUTH_REQUIRED", "Invalid ~/.floom/auth.json. Run: floom login to refresh local auth.");
|
|
2413
2474
|
}
|
|
@@ -2461,14 +2522,17 @@ function allowsCustomApiUrl() {
|
|
|
2461
2522
|
const raw = process.env.FLOOM_ALLOW_CUSTOM_API_URL?.trim().toLowerCase();
|
|
2462
2523
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
2463
2524
|
}
|
|
2464
|
-
function
|
|
2525
|
+
function isCanonicalApiUrl(apiUrl) {
|
|
2465
2526
|
try {
|
|
2466
2527
|
const url = new URL(apiUrl);
|
|
2467
|
-
return TRUSTED_API_HOSTS.has(url.hostname)
|
|
2528
|
+
return TRUSTED_API_HOSTS.has(url.hostname);
|
|
2468
2529
|
} catch {
|
|
2469
2530
|
return false;
|
|
2470
2531
|
}
|
|
2471
2532
|
}
|
|
2533
|
+
function isTrustedApiUrl(apiUrl) {
|
|
2534
|
+
return isCanonicalApiUrl(apiUrl) || allowsCustomApiUrl();
|
|
2535
|
+
}
|
|
2472
2536
|
function trustedApiUrlOrDefault(apiUrl) {
|
|
2473
2537
|
const normalized = normalizeApiUrl(apiUrl);
|
|
2474
2538
|
return isTrustedApiUrl(normalized) ? normalized : DEFAULT_API_URL;
|
|
@@ -2483,11 +2547,12 @@ function isLegacyApiUrl(apiUrl) {
|
|
|
2483
2547
|
}
|
|
2484
2548
|
|
|
2485
2549
|
// src/version.ts
|
|
2486
|
-
var VERSION = "0.2.
|
|
2550
|
+
var VERSION = "0.2.17";
|
|
2487
2551
|
|
|
2488
2552
|
// src/api-client.ts
|
|
2489
2553
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
2490
2554
|
var DEFAULT_RETRY_ATTEMPTS = 2;
|
|
2555
|
+
var MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
2491
2556
|
function timeoutMs() {
|
|
2492
2557
|
const raw = Number(process.env.FLOOM_API_TIMEOUT_MS);
|
|
2493
2558
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
|
|
@@ -2503,7 +2568,7 @@ function retryDelayMs(attempt) {
|
|
|
2503
2568
|
}
|
|
2504
2569
|
async function sleep(ms) {
|
|
2505
2570
|
if (ms <= 0) return;
|
|
2506
|
-
await new Promise((
|
|
2571
|
+
await new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2507
2572
|
}
|
|
2508
2573
|
async function fetchWithTimeout(url, init = {}) {
|
|
2509
2574
|
const controller = new AbortController();
|
|
@@ -2519,12 +2584,32 @@ async function fetchWithTimeout(url, init = {}) {
|
|
|
2519
2584
|
clearTimeout(timer);
|
|
2520
2585
|
}
|
|
2521
2586
|
}
|
|
2522
|
-
function
|
|
2523
|
-
|
|
2524
|
-
|
|
2587
|
+
async function readLimitedText(res, limitBytes = MAX_ERROR_BODY_BYTES) {
|
|
2588
|
+
if (!res.body) return res.text();
|
|
2589
|
+
const reader = res.body.getReader();
|
|
2590
|
+
const chunks = [];
|
|
2591
|
+
let total = 0;
|
|
2592
|
+
let truncated = false;
|
|
2593
|
+
while (true) {
|
|
2594
|
+
const { done, value } = await reader.read();
|
|
2595
|
+
if (done) break;
|
|
2596
|
+
const chunk = value ?? new Uint8Array();
|
|
2597
|
+
if (total + chunk.byteLength > limitBytes) {
|
|
2598
|
+
const remaining = Math.max(limitBytes - total, 0);
|
|
2599
|
+
if (remaining > 0) chunks.push(chunk.slice(0, remaining));
|
|
2600
|
+
truncated = true;
|
|
2601
|
+
await reader.cancel();
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
chunks.push(chunk);
|
|
2605
|
+
total += chunk.byteLength;
|
|
2606
|
+
}
|
|
2607
|
+
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
2608
|
+
return truncated ? `${text}
|
|
2609
|
+
[truncated]` : text;
|
|
2525
2610
|
}
|
|
2526
2611
|
function isRedirect(res) {
|
|
2527
|
-
return res.status >= 300 && res.status < 400;
|
|
2612
|
+
return res.type === "opaqueredirect" || res.status >= 300 && res.status < 400;
|
|
2528
2613
|
}
|
|
2529
2614
|
function isRetryableStatus(status) {
|
|
2530
2615
|
return status === 408 || status === 429 || status >= 500;
|
|
@@ -2534,8 +2619,9 @@ function isRetryableMethod(method) {
|
|
|
2534
2619
|
}
|
|
2535
2620
|
async function api(path, opts = {}) {
|
|
2536
2621
|
const auth = await readAuth();
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2622
|
+
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
2623
|
+
const storedToken = auth?.token;
|
|
2624
|
+
if (opts.authRequired && !opts.tokenOverride && !envToken && !storedToken) {
|
|
2539
2625
|
throw new FloomError("AUTH_REQUIRED", "Not logged in. Run: floom login");
|
|
2540
2626
|
}
|
|
2541
2627
|
let lastError = null;
|
|
@@ -2555,7 +2641,15 @@ async function api(path, opts = {}) {
|
|
|
2555
2641
|
"User-Agent": `floom-cli/${VERSION}`,
|
|
2556
2642
|
"x-floom-cli-version": VERSION
|
|
2557
2643
|
};
|
|
2558
|
-
const requestToken = opts.tokenOverride ??
|
|
2644
|
+
const requestToken = opts.tokenOverride ?? envToken ?? (isCanonicalApiUrl(base) ? storedToken : void 0);
|
|
2645
|
+
if (opts.authRequired && !requestToken) {
|
|
2646
|
+
lastError = new FloomError(
|
|
2647
|
+
"AUTH_REQUIRED",
|
|
2648
|
+
"Refusing to send stored auth token to a custom API URL. Set FLOOM_API_TOKEN explicitly only if you trust that API.",
|
|
2649
|
+
{ apiUrl: base }
|
|
2650
|
+
);
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2559
2653
|
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
|
|
2560
2654
|
let res;
|
|
2561
2655
|
try {
|
|
@@ -2580,17 +2674,25 @@ async function api(path, opts = {}) {
|
|
|
2580
2674
|
throw new FloomError(
|
|
2581
2675
|
"INTERNAL_ERROR",
|
|
2582
2676
|
"Floom API returned an unexpected redirect. Refusing to forward credentials.",
|
|
2583
|
-
{ status: res.status, apiUrl: base
|
|
2677
|
+
{ status: res.status, apiUrl: base }
|
|
2584
2678
|
);
|
|
2585
2679
|
}
|
|
2586
|
-
const text = await res.text();
|
|
2680
|
+
const text = res.ok ? await res.text() : await readLimitedText(res);
|
|
2587
2681
|
let json = null;
|
|
2588
2682
|
try {
|
|
2589
2683
|
json = text ? JSON.parse(text) : null;
|
|
2590
2684
|
} catch {
|
|
2591
2685
|
}
|
|
2686
|
+
const envelopeError = json?.error;
|
|
2687
|
+
if (res.ok && envelopeError && typeof envelopeError === "object" && envelopeError.code) {
|
|
2688
|
+
throw new FloomError(
|
|
2689
|
+
envelopeError.code,
|
|
2690
|
+
typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code),
|
|
2691
|
+
{ status: res.status, requestId: envelopeError.request_id, apiUrl: base }
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2592
2694
|
if (res.ok) return json;
|
|
2593
|
-
const err =
|
|
2695
|
+
const err = envelopeError ?? {};
|
|
2594
2696
|
lastError = new FloomError(
|
|
2595
2697
|
err.code ?? "INTERNAL_ERROR",
|
|
2596
2698
|
err.message ?? `HTTP ${res.status} ${res.statusText}`,
|
|
@@ -2621,7 +2723,7 @@ async function rawPut(url, body, contentType = "application/octet-stream") {
|
|
|
2621
2723
|
if (isRedirect(res)) {
|
|
2622
2724
|
throw new FloomError(
|
|
2623
2725
|
"UPLOAD_FAILED",
|
|
2624
|
-
|
|
2726
|
+
"Upload failed: unexpected redirect."
|
|
2625
2727
|
);
|
|
2626
2728
|
}
|
|
2627
2729
|
if (!res.ok) {
|
|
@@ -2641,7 +2743,7 @@ async function rawGet(url) {
|
|
|
2641
2743
|
if (isRedirect(res)) {
|
|
2642
2744
|
throw new FloomError(
|
|
2643
2745
|
"DOWNLOAD_FAILED",
|
|
2644
|
-
|
|
2746
|
+
"Download failed: unexpected redirect."
|
|
2645
2747
|
);
|
|
2646
2748
|
}
|
|
2647
2749
|
if (!res.ok) {
|
|
@@ -2685,9 +2787,18 @@ async function loginCommand() {
|
|
|
2685
2787
|
const interval = Math.max(2, session.poll_interval_seconds) * 1e3;
|
|
2686
2788
|
while (Date.now() < deadline) {
|
|
2687
2789
|
await new Promise((r) => setTimeout(r, interval));
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2790
|
+
let poll;
|
|
2791
|
+
try {
|
|
2792
|
+
poll = await api(`/cli/sessions/${session.session_id}`, {
|
|
2793
|
+
tokenOverride: session.device_code
|
|
2794
|
+
});
|
|
2795
|
+
} catch (e) {
|
|
2796
|
+
if (e instanceof FloomError && String(e.code) === "DEVICE_PENDING") {
|
|
2797
|
+
process.stdout.write(".");
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
throw e;
|
|
2801
|
+
}
|
|
2691
2802
|
if (poll.status === "approved" && poll.token && poll.handle && poll.email) {
|
|
2692
2803
|
await writeAuth({
|
|
2693
2804
|
token: poll.token,
|
|
@@ -2759,13 +2870,16 @@ async function whoamiCommand() {
|
|
|
2759
2870
|
log.heading("Logged in as:");
|
|
2760
2871
|
log.kv("handle", `@${me.user.handle}`);
|
|
2761
2872
|
log.kv("email", me.user.email);
|
|
2873
|
+
if (me.workspace) {
|
|
2874
|
+
log.kv("workspace", `${me.workspace.slug} (${me.workspace.role})`);
|
|
2875
|
+
}
|
|
2762
2876
|
log.kv("api url", process.env.FLOOM_API_URL ?? auth?.apiUrl ?? "default");
|
|
2763
2877
|
if (envToken) log.kv("auth", "FLOOM_API_TOKEN");
|
|
2764
2878
|
}
|
|
2765
2879
|
|
|
2766
2880
|
// src/commands/init.ts
|
|
2767
2881
|
import { writeFile as writeFile2, stat as stat2 } from "node:fs/promises";
|
|
2768
|
-
import { join as join4, basename } from "node:path";
|
|
2882
|
+
import { join as join4, basename as basename2 } from "node:path";
|
|
2769
2883
|
import prompts from "prompts";
|
|
2770
2884
|
async function pathExists(p) {
|
|
2771
2885
|
try {
|
|
@@ -2777,7 +2891,7 @@ async function pathExists(p) {
|
|
|
2777
2891
|
}
|
|
2778
2892
|
async function initCommand() {
|
|
2779
2893
|
const cwd = process.cwd();
|
|
2780
|
-
const folderName =
|
|
2894
|
+
const folderName = basename2(cwd);
|
|
2781
2895
|
if (await pathExists(join4(cwd, "skill.json"))) {
|
|
2782
2896
|
log.err("skill.json already exists in this folder. Edit it directly or run from a fresh directory.");
|
|
2783
2897
|
process.exit(1);
|
|
@@ -2979,6 +3093,52 @@ async function validateCommand(opts = {}) {
|
|
|
2979
3093
|
// src/commands/publish.ts
|
|
2980
3094
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
2981
3095
|
import { join as join6 } from "node:path";
|
|
3096
|
+
|
|
3097
|
+
// src/lib/publish-urls.ts
|
|
3098
|
+
function trimAppUrl(appUrl) {
|
|
3099
|
+
return appUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
|
|
3100
|
+
}
|
|
3101
|
+
function buildAuthenticatedSkillUrl(appUrl, librarySlug, skillSlug) {
|
|
3102
|
+
const base = trimAppUrl(appUrl);
|
|
3103
|
+
return `${base}/library/${encodeURIComponent(skillSlug)}?lib=${encodeURIComponent(librarySlug)}`;
|
|
3104
|
+
}
|
|
3105
|
+
function buildPublicSkillUrl(appUrl, handle, librarySlug, skillSlug) {
|
|
3106
|
+
const base = trimAppUrl(appUrl);
|
|
3107
|
+
if (librarySlug !== handle) return null;
|
|
3108
|
+
return `${base}/@${handle}/${skillSlug}`;
|
|
3109
|
+
}
|
|
3110
|
+
function buildPublishViewLines(args) {
|
|
3111
|
+
const manageUrl = buildAuthenticatedSkillUrl(args.appUrl, args.refRoot, args.slug);
|
|
3112
|
+
const publicUrl = buildPublicSkillUrl(args.appUrl, args.handle, args.refRoot, args.slug);
|
|
3113
|
+
if (args.visibility === "public") {
|
|
3114
|
+
return {
|
|
3115
|
+
heading: publicUrl ? "View (public):" : "Manage (workspace skill):",
|
|
3116
|
+
primaryUrl: publicUrl ?? manageUrl,
|
|
3117
|
+
...publicUrl ? {} : { note: "Workspace skills stay under their authenticated library URL unless copied to your personal library." },
|
|
3118
|
+
shareUrl: args.shareUrl ?? void 0
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
if (args.visibility === "unlisted") {
|
|
3122
|
+
return {
|
|
3123
|
+
heading: publicUrl ? "View (unlisted - use a share link for unauthenticated access):" : "Manage (unlisted workspace skill):",
|
|
3124
|
+
primaryUrl: publicUrl ?? manageUrl,
|
|
3125
|
+
...publicUrl ? {} : { note: "Use the share link for unauthenticated access; workspace detail URLs remain authenticated." },
|
|
3126
|
+
shareUrl: args.shareUrl ?? void 0
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
heading: "Manage (private - sign in to your workspace):",
|
|
3131
|
+
primaryUrl: manageUrl,
|
|
3132
|
+
...publicUrl ? {
|
|
3133
|
+
secondaryHeading: "Public URL after you make it public or unlisted:",
|
|
3134
|
+
secondaryUrl: publicUrl
|
|
3135
|
+
} : {
|
|
3136
|
+
note: "Workspace skills stay under their authenticated library URL unless copied to your personal library."
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/commands/publish.ts
|
|
2982
3142
|
async function publishCommand(opts = {}) {
|
|
2983
3143
|
const auth = await readAuth();
|
|
2984
3144
|
const envToken = process.env.FLOOM_API_TOKEN?.trim();
|
|
@@ -2986,7 +3146,7 @@ async function publishCommand(opts = {}) {
|
|
|
2986
3146
|
log.err("Not logged in. Run: floom login");
|
|
2987
3147
|
process.exit(1);
|
|
2988
3148
|
}
|
|
2989
|
-
const dir = process.cwd();
|
|
3149
|
+
const dir = opts.dir ?? process.cwd();
|
|
2990
3150
|
log.heading("Validating skill...");
|
|
2991
3151
|
const report = await validateSkill(dir);
|
|
2992
3152
|
if (!report.ok) {
|
|
@@ -3061,26 +3221,126 @@ async function publishCommand(opts = {}) {
|
|
|
3061
3221
|
log.blank();
|
|
3062
3222
|
log.ok(`Published ${complete.ref}`);
|
|
3063
3223
|
log.blank();
|
|
3064
|
-
log.info("View:");
|
|
3065
3224
|
const displayApiUrl = trustedApiUrlOrDefault(process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL);
|
|
3066
|
-
|
|
3225
|
+
const appUrl = displayApiUrl.replace(/\/api\/v1\/?$/, "");
|
|
3226
|
+
const visibility = complete.visibility ?? "private";
|
|
3227
|
+
const views = buildPublishViewLines({
|
|
3228
|
+
visibility,
|
|
3229
|
+
appUrl,
|
|
3230
|
+
handle,
|
|
3231
|
+
slug: manifest.name,
|
|
3232
|
+
refRoot,
|
|
3233
|
+
shareUrl: complete.share_url
|
|
3234
|
+
});
|
|
3235
|
+
log.info(views.heading);
|
|
3236
|
+
log.kv("", views.primaryUrl);
|
|
3237
|
+
if (views.secondaryHeading && views.secondaryUrl) {
|
|
3238
|
+
log.info(views.secondaryHeading);
|
|
3239
|
+
log.kv("", views.secondaryUrl);
|
|
3240
|
+
}
|
|
3241
|
+
if (views.note) {
|
|
3242
|
+
log.info(views.note);
|
|
3243
|
+
}
|
|
3244
|
+
if (views.shareUrl) {
|
|
3245
|
+
log.info("Share link:");
|
|
3246
|
+
log.kv("", views.shareUrl);
|
|
3247
|
+
} else if (visibility === "unlisted") {
|
|
3248
|
+
log.info(`Create a share link: floom link create ${refRoot}/${manifest.name}`);
|
|
3249
|
+
}
|
|
3067
3250
|
log.info("Install:");
|
|
3068
3251
|
log.kv("", complete.install_command);
|
|
3069
3252
|
}
|
|
3070
3253
|
|
|
3254
|
+
// src/commands/push.ts
|
|
3255
|
+
import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3256
|
+
import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
|
|
3257
|
+
function parsePushConcurrency(value) {
|
|
3258
|
+
const raw = value ?? 6;
|
|
3259
|
+
const parsed = typeof raw === "number" ? raw : Number.parseInt(raw, 10);
|
|
3260
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 16) {
|
|
3261
|
+
throw new Error("--concurrency must be an integer from 1 to 16.");
|
|
3262
|
+
}
|
|
3263
|
+
return parsed;
|
|
3264
|
+
}
|
|
3265
|
+
async function hasSkillFiles(dir) {
|
|
3266
|
+
try {
|
|
3267
|
+
const [skillMd, manifest] = await Promise.all([
|
|
3268
|
+
stat3(join7(dir, "SKILL.md")),
|
|
3269
|
+
stat3(join7(dir, "skill.json"))
|
|
3270
|
+
]);
|
|
3271
|
+
return skillMd.isFile() && manifest.isFile();
|
|
3272
|
+
} catch {
|
|
3273
|
+
return false;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
async function findImmediateSkillDirs(root) {
|
|
3277
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
3278
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => join7(root, entry.name)).sort();
|
|
3279
|
+
const checks = await Promise.all(dirs.map(async (dir) => await hasSkillFiles(dir) ? dir : null));
|
|
3280
|
+
return checks.filter((dir) => Boolean(dir));
|
|
3281
|
+
}
|
|
3282
|
+
async function runBounded(items, concurrency, worker) {
|
|
3283
|
+
let next = 0;
|
|
3284
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
3285
|
+
while (next < items.length) {
|
|
3286
|
+
const item = items[next];
|
|
3287
|
+
next += 1;
|
|
3288
|
+
await worker(item);
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
3291
|
+
await Promise.all(workers);
|
|
3292
|
+
}
|
|
3293
|
+
async function pushCommand(dir = ".", options = {}, deps = {}) {
|
|
3294
|
+
const root = resolve2(dir);
|
|
3295
|
+
const rootStat = await stat3(root).catch(() => null);
|
|
3296
|
+
if (!rootStat?.isDirectory()) {
|
|
3297
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
3298
|
+
}
|
|
3299
|
+
const publish = deps.publish ?? publishCommand;
|
|
3300
|
+
if (await hasSkillFiles(root)) {
|
|
3301
|
+
await publish({ ...options, dir: root });
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
const skillDirs = await findImmediateSkillDirs(root);
|
|
3305
|
+
if (skillDirs.length === 0) {
|
|
3306
|
+
throw new Error("SKILL.md and skill.json are required, either in this directory or immediate child directories.");
|
|
3307
|
+
}
|
|
3308
|
+
const concurrency = parsePushConcurrency(options.concurrency);
|
|
3309
|
+
const startedAt = Date.now();
|
|
3310
|
+
const errors = [];
|
|
3311
|
+
let pushed = 0;
|
|
3312
|
+
await runBounded(skillDirs, concurrency, async (skillDir) => {
|
|
3313
|
+
const slug = basename3(skillDir);
|
|
3314
|
+
try {
|
|
3315
|
+
await publish({ ...options, dir: skillDir });
|
|
3316
|
+
pushed += 1;
|
|
3317
|
+
log.info(`Pushed ${slug}`);
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
errors.push({ slug, message: error.message });
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
const elapsed = ((Date.now() - startedAt) / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
3323
|
+
log.info(`Pushed ${pushed}/${skillDirs.length} skills in ${elapsed}s.`);
|
|
3324
|
+
if (errors.length > 0) {
|
|
3325
|
+
log.err("Push errors:");
|
|
3326
|
+
for (const error of errors) log.err(`- ${error.slug}: ${error.message}`);
|
|
3327
|
+
process.exitCode = 1;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3071
3331
|
// src/commands/install.ts
|
|
3072
|
-
import { mkdir as mkdir4, readdir as
|
|
3073
|
-
import { join as
|
|
3074
|
-
import { tmpdir } from "node:os";
|
|
3332
|
+
import { mkdir as mkdir4, readdir as readdir4, rm, rename } from "node:fs/promises";
|
|
3333
|
+
import { join as join9 } from "node:path";
|
|
3334
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3075
3335
|
|
|
3076
3336
|
// src/lib/floom-lock.ts
|
|
3077
|
-
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as
|
|
3078
|
-
import { join as
|
|
3337
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3, stat as stat4, readdir as readdir3 } from "node:fs/promises";
|
|
3338
|
+
import { join as join8, relative as relative3, sep as sep2, posix as posix2 } from "node:path";
|
|
3079
3339
|
import { createHash as createHash3 } from "node:crypto";
|
|
3080
3340
|
var EMPTY = { schema_version: "0.1", skills: {} };
|
|
3081
3341
|
async function readLock(projectDir) {
|
|
3082
3342
|
try {
|
|
3083
|
-
const raw = await readFile7(
|
|
3343
|
+
const raw = await readFile7(join8(projectDir, "floom.lock"), "utf8");
|
|
3084
3344
|
const parsed = JSON.parse(raw);
|
|
3085
3345
|
if (parsed.schema_version === "0.1") return parsed;
|
|
3086
3346
|
if (parsed.schema_version === "0.2") return parsed;
|
|
@@ -3095,7 +3355,7 @@ async function readLock(projectDir) {
|
|
|
3095
3355
|
}
|
|
3096
3356
|
async function writeLock(projectDir, lock) {
|
|
3097
3357
|
await mkdir3(projectDir, { recursive: true });
|
|
3098
|
-
await writeFile3(
|
|
3358
|
+
await writeFile3(join8(projectDir, "floom.lock"), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
3099
3359
|
}
|
|
3100
3360
|
function setLockEntry(lock, ref, entry) {
|
|
3101
3361
|
return { ...lock, skills: { ...lock.skills, [ref]: entry } };
|
|
@@ -3125,12 +3385,12 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3125
3385
|
async function walk2(dir) {
|
|
3126
3386
|
let entries;
|
|
3127
3387
|
try {
|
|
3128
|
-
entries = await
|
|
3388
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
3129
3389
|
} catch {
|
|
3130
3390
|
return;
|
|
3131
3391
|
}
|
|
3132
3392
|
for (const entry of entries) {
|
|
3133
|
-
const abs =
|
|
3393
|
+
const abs = join8(dir, entry.name);
|
|
3134
3394
|
if (entry.isSymbolicLink()) continue;
|
|
3135
3395
|
if (entry.isDirectory()) {
|
|
3136
3396
|
await walk2(abs);
|
|
@@ -3138,7 +3398,7 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3138
3398
|
}
|
|
3139
3399
|
if (!entry.isFile()) continue;
|
|
3140
3400
|
const buf = await readFile7(abs);
|
|
3141
|
-
const rel =
|
|
3401
|
+
const rel = relative3(folderAbs, abs).split(sep2).join(posix2.sep);
|
|
3142
3402
|
out.push({
|
|
3143
3403
|
relPath: rel,
|
|
3144
3404
|
sha256: createHash3("sha256").update(buf).digest("hex"),
|
|
@@ -3147,7 +3407,7 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3147
3407
|
}
|
|
3148
3408
|
}
|
|
3149
3409
|
try {
|
|
3150
|
-
await
|
|
3410
|
+
await stat4(folderAbs);
|
|
3151
3411
|
} catch {
|
|
3152
3412
|
return out;
|
|
3153
3413
|
}
|
|
@@ -3156,6 +3416,20 @@ async function hashInstalledFolder(folderAbs) {
|
|
|
3156
3416
|
return out;
|
|
3157
3417
|
}
|
|
3158
3418
|
|
|
3419
|
+
// src/lib/filesystem-safety.ts
|
|
3420
|
+
import { lstat as lstat2 } from "node:fs/promises";
|
|
3421
|
+
async function assertPathIsNotSymlink(path, label = "path") {
|
|
3422
|
+
try {
|
|
3423
|
+
const stats = await lstat2(path);
|
|
3424
|
+
if (stats.isSymbolicLink()) {
|
|
3425
|
+
throw new Error(`${label} must not be a symbolic link: ${path}`);
|
|
3426
|
+
}
|
|
3427
|
+
} catch (e) {
|
|
3428
|
+
if (e.code === "ENOENT") return;
|
|
3429
|
+
throw e;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3159
3433
|
// src/commands/install.ts
|
|
3160
3434
|
var CLI_VERSION = VERSION;
|
|
3161
3435
|
function semverGte(a, b) {
|
|
@@ -3209,13 +3483,16 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3209
3483
|
global: opts.global
|
|
3210
3484
|
});
|
|
3211
3485
|
const projectDir = opts.global ? process.cwd() : process.cwd();
|
|
3212
|
-
const
|
|
3486
|
+
const parentDir = opts.to ? normalizeInstallParentDir(target.dir, ref.slug) : target.dir;
|
|
3487
|
+
const destFolder = join9(parentDir, ref.slug);
|
|
3488
|
+
await assertPathIsNotSymlink(parentDir, "install parent directory");
|
|
3489
|
+
await assertPathIsNotSymlink(destFolder, "install destination");
|
|
3213
3490
|
const lock = await readLock(projectDir);
|
|
3214
3491
|
const refKey = formatSkillRef({ owner: ref.owner, slug: ref.slug });
|
|
3215
3492
|
const existing = lock.skills[refKey];
|
|
3216
3493
|
let folderExists = false;
|
|
3217
3494
|
try {
|
|
3218
|
-
const entries = await
|
|
3495
|
+
const entries = await readdir4(destFolder);
|
|
3219
3496
|
folderExists = entries.length > 0;
|
|
3220
3497
|
} catch {
|
|
3221
3498
|
}
|
|
@@ -3245,7 +3522,7 @@ async function installCommand(refStr, opts = {}) {
|
|
|
3245
3522
|
}
|
|
3246
3523
|
log.ok(`Bundle verified (${(buf.length / 1024).toFixed(1)} KB).`);
|
|
3247
3524
|
const tmp = await import("node:fs/promises").then(
|
|
3248
|
-
(m) => m.mkdtemp(
|
|
3525
|
+
(m) => m.mkdtemp(join9(tmpdir2(), `floom-install-${ref.slug}-`))
|
|
3249
3526
|
);
|
|
3250
3527
|
try {
|
|
3251
3528
|
await extractBundle(buf, tmp);
|
|
@@ -3348,8 +3625,8 @@ async function outdatedCommand() {
|
|
|
3348
3625
|
|
|
3349
3626
|
// src/commands/update.ts
|
|
3350
3627
|
import { rm as rm2, rename as rename2, mkdir as mkdir5 } from "node:fs/promises";
|
|
3351
|
-
import { tmpdir as
|
|
3352
|
-
import { join as
|
|
3628
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
3629
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
3353
3630
|
function cmpSemver2(a, b) {
|
|
3354
3631
|
const pa = a.split(".").map((p) => parseInt(p, 10));
|
|
3355
3632
|
const pb = b.split(".").map((p) => parseInt(p, 10));
|
|
@@ -3388,7 +3665,8 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3388
3665
|
log.step(`${ref} is already at latest (${entry.version}).`);
|
|
3389
3666
|
continue;
|
|
3390
3667
|
}
|
|
3391
|
-
const installDir =
|
|
3668
|
+
const installDir = resolve4(projectDir, entry.path);
|
|
3669
|
+
await assertPathIsNotSymlink(installDir, "installed skill directory");
|
|
3392
3670
|
if (!opts.force) {
|
|
3393
3671
|
const current = await hashInstalledFolder(installDir);
|
|
3394
3672
|
log.step(`Updating ${ref}: ${entry.version} -> ${info.latest_version}`);
|
|
@@ -3401,7 +3679,7 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3401
3679
|
continue;
|
|
3402
3680
|
}
|
|
3403
3681
|
const tmp = await import("node:fs/promises").then(
|
|
3404
|
-
(m) => m.mkdtemp(
|
|
3682
|
+
(m) => m.mkdtemp(join10(tmpdir3(), `floom-update-${slug}-`))
|
|
3405
3683
|
);
|
|
3406
3684
|
try {
|
|
3407
3685
|
await extractBundle(buf, tmp);
|
|
@@ -3528,6 +3806,26 @@ async function unshareCommand(refStr, email) {
|
|
|
3528
3806
|
log.ok(`Removed ${email}'s access to ${refStr}.`);
|
|
3529
3807
|
}
|
|
3530
3808
|
|
|
3809
|
+
// src/commands/link.ts
|
|
3810
|
+
async function linkCreateCommand(refStr, opts = {}) {
|
|
3811
|
+
const ref = parseSkillRef(refStr);
|
|
3812
|
+
if (!ref) {
|
|
3813
|
+
log.err(`Invalid skill ref: ${refStr}`);
|
|
3814
|
+
process.exit(1);
|
|
3815
|
+
}
|
|
3816
|
+
const role = opts.role ?? "viewer";
|
|
3817
|
+
const body = { role };
|
|
3818
|
+
if (opts.name) body.name = opts.name;
|
|
3819
|
+
const r = await api(`/skills/${ref.owner}/${ref.slug}/links`, {
|
|
3820
|
+
method: "POST",
|
|
3821
|
+
authRequired: true,
|
|
3822
|
+
body
|
|
3823
|
+
});
|
|
3824
|
+
log.ok(`Share link created for ${refStr}.`);
|
|
3825
|
+
log.kv("url", r.link.url);
|
|
3826
|
+
log.kv("role", r.link.role);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3531
3829
|
// src/commands/library.ts
|
|
3532
3830
|
function isValidEmail2(email) {
|
|
3533
3831
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
@@ -3597,7 +3895,7 @@ async function workspaceAcceptCommand(tokenOrUrl) {
|
|
|
3597
3895
|
// src/lib/config-file.ts
|
|
3598
3896
|
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
3599
3897
|
import { homedir as homedir3 } from "node:os";
|
|
3600
|
-
import { dirname, join as
|
|
3898
|
+
import { dirname as dirname2, join as join11 } from "node:path";
|
|
3601
3899
|
import { z as z2 } from "zod";
|
|
3602
3900
|
var FLOOM_CONFIG_VERSION = "0.1";
|
|
3603
3901
|
var TARGETS = ["claude", "codex", "cursor", "kimi", "opencode"];
|
|
@@ -3611,8 +3909,8 @@ var configSchema = z2.object({
|
|
|
3611
3909
|
targets: z2.record(z2.enum(TARGETS), targetConfigSchema).default({})
|
|
3612
3910
|
});
|
|
3613
3911
|
function configPath(scope, cwd = process.cwd()) {
|
|
3614
|
-
if (scope === "global") return
|
|
3615
|
-
return
|
|
3912
|
+
if (scope === "global") return join11(homedir3(), ".floom", "config.json");
|
|
3913
|
+
return join11(cwd, ".floom", "config.json");
|
|
3616
3914
|
}
|
|
3617
3915
|
async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
3618
3916
|
try {
|
|
@@ -3626,7 +3924,7 @@ async function readFloomConfig(scope, cwd = process.cwd()) {
|
|
|
3626
3924
|
async function writeFloomConfig(scope, config, cwd = process.cwd()) {
|
|
3627
3925
|
const path = configPath(scope, cwd);
|
|
3628
3926
|
const parsed = configSchema.parse(config);
|
|
3629
|
-
await mkdir6(
|
|
3927
|
+
await mkdir6(dirname2(path), { recursive: true, mode: 448 });
|
|
3630
3928
|
await writeFile4(path, JSON.stringify(parsed, null, 2) + "\n", { mode: 384 });
|
|
3631
3929
|
}
|
|
3632
3930
|
function normalizeScope(value) {
|
|
@@ -3664,7 +3962,7 @@ async function setWorkspaceActive(input) {
|
|
|
3664
3962
|
|
|
3665
3963
|
// src/commands/instruction.ts
|
|
3666
3964
|
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
|
|
3667
|
-
import { basename as
|
|
3965
|
+
import { basename as basename4, dirname as dirname3, join as join12 } from "node:path";
|
|
3668
3966
|
import { createHash as createHash4 } from "node:crypto";
|
|
3669
3967
|
var START = "<!-- FLOOM START -->";
|
|
3670
3968
|
var END = "<!-- FLOOM END -->";
|
|
@@ -3749,7 +4047,7 @@ async function writeManagedInstructionFile(input) {
|
|
|
3749
4047
|
else throw e;
|
|
3750
4048
|
}
|
|
3751
4049
|
const next = replaceManagedBlock(existing, input.blockBody);
|
|
3752
|
-
if (existed && !next.hadBlock && !input.apply
|
|
4050
|
+
if (existed && !next.hadBlock && !input.apply) {
|
|
3753
4051
|
log.warn(`Refusing to modify ${input.path} without an existing Floom managed block.`);
|
|
3754
4052
|
log.info("Re-run with --apply to append the managed block, or --path <file> for a dedicated file.");
|
|
3755
4053
|
process.exit(1);
|
|
@@ -3758,11 +4056,14 @@ async function writeManagedInstructionFile(input) {
|
|
|
3758
4056
|
let backupPath;
|
|
3759
4057
|
if (existed && existing) {
|
|
3760
4058
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3761
|
-
backupPath =
|
|
3762
|
-
await
|
|
4059
|
+
backupPath = join12(".floom", "backups", `${basename4(input.path)}.${stamp}.bak`);
|
|
4060
|
+
await assertPathIsNotSymlink(dirname3(backupPath), "instruction backup directory");
|
|
4061
|
+
await assertPathIsNotSymlink(backupPath, "instruction backup file");
|
|
4062
|
+
await mkdir7(dirname3(backupPath), { recursive: true, mode: 448 });
|
|
3763
4063
|
await writeFile5(backupPath, existing, { mode: 384 });
|
|
3764
4064
|
}
|
|
3765
|
-
await mkdir7(
|
|
4065
|
+
await mkdir7(dirname3(input.path), { recursive: true });
|
|
4066
|
+
await assertPathIsNotSymlink(input.path, "instruction file");
|
|
3766
4067
|
await writeFile5(input.path, next.content, "utf8");
|
|
3767
4068
|
return { changed: true, backupPath };
|
|
3768
4069
|
}
|
|
@@ -3915,7 +4216,7 @@ async function workspaceActiveCommand(opts = {}) {
|
|
|
3915
4216
|
|
|
3916
4217
|
// src/commands/sync.ts
|
|
3917
4218
|
import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
|
|
3918
|
-
import { dirname as
|
|
4219
|
+
import { dirname as dirname4, join as join13 } from "node:path";
|
|
3919
4220
|
var ROUTER_SKILL = [
|
|
3920
4221
|
"# Floom Find Skills",
|
|
3921
4222
|
"",
|
|
@@ -3934,8 +4235,8 @@ var ROUTER_SKILL = [
|
|
|
3934
4235
|
async function installRouter(target) {
|
|
3935
4236
|
const install = resolveInstallDir({ target });
|
|
3936
4237
|
const routerDir = target === "kimi" ? "floom" : "floom-find-skills";
|
|
3937
|
-
const path =
|
|
3938
|
-
await mkdir8(
|
|
4238
|
+
const path = join13(install.dir, routerDir, "SKILL.md");
|
|
4239
|
+
await mkdir8(dirname4(path), { recursive: true });
|
|
3939
4240
|
await writeFile6(path, ROUTER_SKILL, "utf8");
|
|
3940
4241
|
return path;
|
|
3941
4242
|
}
|
|
@@ -4203,13 +4504,14 @@ async function folderRevokeCommand(workspace, folderId, memberId) {
|
|
|
4203
4504
|
}
|
|
4204
4505
|
|
|
4205
4506
|
// src/commands/mcp.ts
|
|
4206
|
-
import { mkdtemp, mkdir as mkdir9, readdir as
|
|
4207
|
-
import { join as
|
|
4208
|
-
import { tmpdir as
|
|
4507
|
+
import { mkdtemp, mkdir as mkdir9, readdir as readdir5, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4508
|
+
import { join as join14 } from "node:path";
|
|
4509
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
4209
4510
|
import { z as z3 } from "zod";
|
|
4210
4511
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4211
4512
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4212
4513
|
var API_TIMEOUT_MS = 2e4;
|
|
4514
|
+
var MAX_ERROR_BODY_BYTES2 = 64 * 1024;
|
|
4213
4515
|
function semverGte2(a, b) {
|
|
4214
4516
|
const pa = a.split(".").map(Number);
|
|
4215
4517
|
const pb = b.split(".").map(Number);
|
|
@@ -4231,6 +4533,30 @@ async function fetchWithTimeout2(url, init = {}) {
|
|
|
4231
4533
|
clearTimeout(timer);
|
|
4232
4534
|
}
|
|
4233
4535
|
}
|
|
4536
|
+
async function readLimitedText2(res, limitBytes = MAX_ERROR_BODY_BYTES2) {
|
|
4537
|
+
if (!res.body) return res.text();
|
|
4538
|
+
const reader = res.body.getReader();
|
|
4539
|
+
const chunks = [];
|
|
4540
|
+
let total = 0;
|
|
4541
|
+
let truncated = false;
|
|
4542
|
+
while (true) {
|
|
4543
|
+
const { done, value } = await reader.read();
|
|
4544
|
+
if (done) break;
|
|
4545
|
+
const chunk = value ?? new Uint8Array();
|
|
4546
|
+
if (total + chunk.byteLength > limitBytes) {
|
|
4547
|
+
const remaining = Math.max(limitBytes - total, 0);
|
|
4548
|
+
if (remaining > 0) chunks.push(chunk.slice(0, remaining));
|
|
4549
|
+
truncated = true;
|
|
4550
|
+
await reader.cancel();
|
|
4551
|
+
break;
|
|
4552
|
+
}
|
|
4553
|
+
chunks.push(chunk);
|
|
4554
|
+
total += chunk.byteLength;
|
|
4555
|
+
}
|
|
4556
|
+
const text = new TextDecoder().decode(Buffer.concat(chunks));
|
|
4557
|
+
return truncated ? `${text}
|
|
4558
|
+
[truncated]` : text;
|
|
4559
|
+
}
|
|
4234
4560
|
async function resolveOptionalToken() {
|
|
4235
4561
|
const fromEnv = process.env.FLOOM_API_TOKEN?.trim();
|
|
4236
4562
|
if (fromEnv) return fromEnv;
|
|
@@ -4263,19 +4589,21 @@ async function apiRequest(token, path, query) {
|
|
|
4263
4589
|
lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
|
|
4264
4590
|
continue;
|
|
4265
4591
|
}
|
|
4266
|
-
if (res.status >= 300 && res.status < 400) {
|
|
4267
|
-
|
|
4268
|
-
const redirectHost2 = location ? new URL(location, url).host : "unknown";
|
|
4269
|
-
lastError = new Error(`Floom API returned an unexpected redirect to ${redirectHost2}. Refusing to forward credentials.`);
|
|
4592
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
4593
|
+
lastError = new Error("Floom API returned an unexpected redirect. Refusing to forward credentials.");
|
|
4270
4594
|
break;
|
|
4271
4595
|
}
|
|
4272
|
-
const body = await res.text();
|
|
4596
|
+
const body = res.ok ? await res.text() : await readLimitedText2(res);
|
|
4273
4597
|
let json = null;
|
|
4274
4598
|
try {
|
|
4275
4599
|
json = body ? JSON.parse(body) : null;
|
|
4276
4600
|
} catch {
|
|
4277
4601
|
json = { raw: body };
|
|
4278
4602
|
}
|
|
4603
|
+
if (json?.error?.code) {
|
|
4604
|
+
lastError = new Error(json.error.message ?? String(json.error.code));
|
|
4605
|
+
if (res.ok || res.status !== 404) break;
|
|
4606
|
+
}
|
|
4279
4607
|
if (res.ok) return json;
|
|
4280
4608
|
lastError = new Error(json?.error?.message ?? `HTTP ${res.status}`);
|
|
4281
4609
|
if (res.status !== 404 || json?.error) break;
|
|
@@ -4294,14 +4622,16 @@ async function installViaApi(token, refText, target, options = {}) {
|
|
|
4294
4622
|
const bundle = await rawGet(dl.download.url);
|
|
4295
4623
|
if (!verifyBundleHash(bundle, dl.bundle_sha256)) throw new Error("Bundle hash mismatch");
|
|
4296
4624
|
const install = resolveInstallDir({ target });
|
|
4625
|
+
await assertPathIsNotSymlink(install.dir, "install directory");
|
|
4297
4626
|
await mkdir9(install.dir, { recursive: true });
|
|
4298
|
-
const dest =
|
|
4299
|
-
|
|
4627
|
+
const dest = join14(install.dir, parsed.slug);
|
|
4628
|
+
await assertPathIsNotSymlink(dest, "install destination");
|
|
4629
|
+
const exists = await readdir5(dest).then(() => true).catch(() => false);
|
|
4300
4630
|
if (exists && !options.force) {
|
|
4301
4631
|
throw new Error(`Folder already exists at ${dest}. Call install_skill with force=true to overwrite after reviewing local changes.`);
|
|
4302
4632
|
}
|
|
4303
4633
|
if (exists) await rm3(dest, { recursive: true, force: true });
|
|
4304
|
-
const temp = await mkdtemp(
|
|
4634
|
+
const temp = await mkdtemp(join14(tmpdir4(), `floom-mcp-${parsed.slug}-`));
|
|
4305
4635
|
try {
|
|
4306
4636
|
await extractBundle(bundle, temp);
|
|
4307
4637
|
await rename3(temp, dest);
|
|
@@ -4324,24 +4654,24 @@ async function installViaApi(token, refText, target, options = {}) {
|
|
|
4324
4654
|
return { path: dest, version: dl.version, ref: info.ref ?? ref, has_scripts: !!dl.has_scripts };
|
|
4325
4655
|
}
|
|
4326
4656
|
async function parseSkillBundle(bundle) {
|
|
4327
|
-
const tmp = await mkdtemp(
|
|
4657
|
+
const tmp = await mkdtemp(join14(tmpdir4(), "floom-mcp-read-"));
|
|
4328
4658
|
try {
|
|
4329
4659
|
await extractBundle(bundle, tmp);
|
|
4330
4660
|
const files = [];
|
|
4331
4661
|
const walk2 = async (dir, rel = "") => {
|
|
4332
|
-
const entries = await
|
|
4662
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
4333
4663
|
for (const entry of entries) {
|
|
4334
4664
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4335
|
-
const full =
|
|
4665
|
+
const full = join14(dir, entry.name);
|
|
4336
4666
|
if (entry.isDirectory()) await walk2(full, nextRel);
|
|
4337
4667
|
else files.push(nextRel);
|
|
4338
4668
|
}
|
|
4339
4669
|
};
|
|
4340
4670
|
await walk2(tmp);
|
|
4341
4671
|
const skillMdPath = files.find((f) => f.toUpperCase() === "SKILL.MD");
|
|
4342
|
-
const skillMd = skillMdPath ? await readFile10(
|
|
4672
|
+
const skillMd = skillMdPath ? await readFile10(join14(tmp, skillMdPath), "utf8") : "";
|
|
4343
4673
|
const skillJsonPath = files.find((f) => f.toLowerCase() === "skill.json");
|
|
4344
|
-
const skillJson = skillJsonPath ? JSON.parse(await readFile10(
|
|
4674
|
+
const skillJson = skillJsonPath ? JSON.parse(await readFile10(join14(tmp, skillJsonPath), "utf8")) : null;
|
|
4345
4675
|
return { files, skill_md: skillMd, skill_json: skillJson };
|
|
4346
4676
|
} finally {
|
|
4347
4677
|
await rm3(tmp, { recursive: true, force: true });
|
|
@@ -4434,11 +4764,29 @@ async function mcpCommand() {
|
|
|
4434
4764
|
}
|
|
4435
4765
|
|
|
4436
4766
|
// src/commands/doctor.ts
|
|
4437
|
-
import { mkdtemp as mkdtemp2, readdir as
|
|
4438
|
-
import { tmpdir as
|
|
4439
|
-
import { join as
|
|
4767
|
+
import { mkdtemp as mkdtemp2, readdir as readdir6, rm as rm4 } from "node:fs/promises";
|
|
4768
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
4769
|
+
import { join as join15 } from "node:path";
|
|
4770
|
+
import { fileURLToPath } from "node:url";
|
|
4440
4771
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4441
4772
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4773
|
+
|
|
4774
|
+
// src/lib/api-health.ts
|
|
4775
|
+
async function probeApiHealth(rawApiUrl) {
|
|
4776
|
+
const apiV1 = trustedApiUrlOrDefault(rawApiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
4777
|
+
const healthUrl = `${apiV1}/health`;
|
|
4778
|
+
try {
|
|
4779
|
+
const res = await fetch(healthUrl, { method: "GET", signal: AbortSignal.timeout(8e3) });
|
|
4780
|
+
if (!res.ok) return { name: "api_health", ok: false, detail: `GET ${healthUrl} \u2192 HTTP ${res.status}` };
|
|
4781
|
+
const body = await res.json();
|
|
4782
|
+
if (body.ok !== true) return { name: "api_health", ok: false, detail: `unexpected JSON from ${healthUrl}` };
|
|
4783
|
+
return { name: "api_health", ok: true, detail: healthUrl };
|
|
4784
|
+
} catch (e) {
|
|
4785
|
+
return { name: "api_health", ok: false, detail: `${healthUrl}: ${e.message}` };
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
// src/commands/doctor.ts
|
|
4442
4790
|
function textOf(result) {
|
|
4443
4791
|
return String(result?.content?.[0]?.text ?? "");
|
|
4444
4792
|
}
|
|
@@ -4502,11 +4850,13 @@ async function doctorCommand(opts = {}) {
|
|
|
4502
4850
|
authCheck2 = fail("auth", `saved login rejected by API: ${e.message}`);
|
|
4503
4851
|
}
|
|
4504
4852
|
}
|
|
4853
|
+
const resolvedApiUrl2 = process.env.FLOOM_API_URL ?? auth2?.apiUrl ?? DEFAULT_API_URL;
|
|
4505
4854
|
const checks2 = [
|
|
4506
4855
|
pass("cli_version", VERSION),
|
|
4507
4856
|
authCheck2,
|
|
4508
4857
|
process.env.FLOOM_API_URL ? apiUrlCheck(process.env.FLOOM_API_URL) : isLegacyApiUrl(rawAuth?.apiUrl) ? warn("auth_api_url", `legacy URL in ~/.floom/auth.json; using ${DEFAULT_API_URL}`) : apiUrlCheck(auth2?.apiUrl ?? DEFAULT_API_URL)
|
|
4509
4858
|
];
|
|
4859
|
+
checks2.push(await probeApiHealth(resolvedApiUrl2));
|
|
4510
4860
|
emitDoctor(checks2, opts.json);
|
|
4511
4861
|
if (checks2.some((check) => !check.ok)) process.exit(1);
|
|
4512
4862
|
return;
|
|
@@ -4514,18 +4864,20 @@ async function doctorCommand(opts = {}) {
|
|
|
4514
4864
|
const checks = [];
|
|
4515
4865
|
const auth = await readAuth();
|
|
4516
4866
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
4867
|
+
const resolvedApiUrl = process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL;
|
|
4868
|
+
checks.push(await probeApiHealth(resolvedApiUrl));
|
|
4517
4869
|
const authCheck = await validateCurrentToken(token);
|
|
4518
4870
|
const hasValidToken = authCheck.ok && authCheck.status !== "warn" && Boolean(token);
|
|
4519
4871
|
checks.push(authCheck);
|
|
4520
|
-
const cliPath =
|
|
4872
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
4521
4873
|
if (!cliPath) {
|
|
4522
|
-
checks.push(fail("fresh_agent_cli_path", "
|
|
4874
|
+
checks.push(fail("fresh_agent_cli_path", "current CLI module path is empty"));
|
|
4523
4875
|
emitDoctor(checks, opts.json);
|
|
4524
4876
|
process.exit(1);
|
|
4525
4877
|
}
|
|
4526
|
-
const tmpHome = await mkdtemp2(
|
|
4527
|
-
const tmpSkills = await mkdtemp2(
|
|
4528
|
-
const tmpProject = await mkdtemp2(
|
|
4878
|
+
const tmpHome = await mkdtemp2(join15(tmpdir5(), "floom-doctor-home-"));
|
|
4879
|
+
const tmpSkills = await mkdtemp2(join15(tmpdir5(), "floom-doctor-skills-"));
|
|
4880
|
+
const tmpProject = await mkdtemp2(join15(tmpdir5(), "floom-doctor-project-"));
|
|
4529
4881
|
const transport = new StdioClientTransport({
|
|
4530
4882
|
command: process.execPath,
|
|
4531
4883
|
args: [cliPath, "mcp"],
|
|
@@ -4565,7 +4917,7 @@ async function doctorCommand(opts = {}) {
|
|
|
4565
4917
|
const skill = await client.callTool({ name: "get_skill", arguments: { ref: opts.ref } });
|
|
4566
4918
|
checks.push(/SKILL\.md/.test(textOf(skill)) ? pass("mcp_get_skill", opts.ref) : fail("mcp_get_skill", "bundle did not include SKILL.md"));
|
|
4567
4919
|
const installed = await client.callTool({ name: "install_skill", arguments: { ref: opts.ref, target: opts.target ?? "codex" } });
|
|
4568
|
-
const entries = await
|
|
4920
|
+
const entries = await readdir6(tmpSkills);
|
|
4569
4921
|
checks.push(entries.length > 0 ? pass("mcp_install_skill", textOf(installed)) : fail("mcp_install_skill", "target directory is empty"));
|
|
4570
4922
|
} else {
|
|
4571
4923
|
checks.push(warn("mcp_public_skill", "skipped; pass --ref <public-skill-ref> to verify get_skill/install_skill against a known public skill"));
|
|
@@ -4605,8 +4957,8 @@ program.command("whoami").description("Show the logged-in user.").action(whoamiC
|
|
|
4605
4957
|
program.command("init").description("Scaffold a new skill in the current directory.").action(initCommand);
|
|
4606
4958
|
program.command("validate").description("Validate the skill in the current directory.").option("--json", "Emit machine-readable JSON").action((opts) => validateCommand(opts));
|
|
4607
4959
|
program.command("publish").description("Publish the skill in the current directory.").option("--dry-run", "Validate and pack locally without uploading.").option("--workspace <slug>", "Publish into a shared workspace slug (default: personal)").option("--library <slug>", "Legacy alias for --workspace").addHelpText("after", "\nExamples:\n $ floom publish\n $ floom publish --workspace team-workspace").action((opts) => publishCommand(opts));
|
|
4608
|
-
program.command("push").description("
|
|
4609
|
-
program.command("install <ref>").description("Install a skill (default: .agents/skills/<slug>/).").option("--for <target>", "Tool preset: claude | codex | cursor | gemini | opencode | kimi | all").option("--to <path>", "
|
|
4960
|
+
program.command("push [dir]").description("Publish one skill folder or every immediate child skill folder.").option("--dry-run", "Validate and pack locally without uploading.").option("--workspace <slug>", "Publish into a shared workspace slug").option("--library <slug>", "Legacy alias for --workspace").option("--concurrency <n>", "Bulk push concurrency, 1-16", "6").addHelpText("after", "\nExamples:\n $ floom push\n $ floom push ./skills --workspace team-workspace --concurrency 4").action((dir, opts) => pushCommand(dir ?? ".", opts));
|
|
4961
|
+
program.command("install <ref>").description("Install a skill (default: .agents/skills/<slug>/).").option("--for <target>", "Tool preset: claude | codex | cursor | gemini | opencode | kimi | all").option("--to <path>", "Parent directory; installs to <path>/<skill-slug>/ (not the skill folder itself)").option("--global", "Install to user-level folder instead of project-local").option("--force", "Overwrite existing folder").addHelpText("after", "\nExamples:\n $ floom install @alice/research-brief\n $ floom install @alice/research-brief --for codex\n $ floom install @alice/research-brief --to .agents/skills\n\nNote: --to is the parent folder. The skill lands in .agents/skills/research-brief/, not directly in .agents/skills/.").action((ref, opts) => installCommand(ref, opts));
|
|
4610
4962
|
program.command("installed").description("List installed skills in this project.").option("--json").action(installedCommand);
|
|
4611
4963
|
program.command("outdated").description("Show installed skills with newer versions available.").action(outdatedCommand);
|
|
4612
4964
|
program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
|
|
@@ -4619,6 +4971,8 @@ program.command("pinned").alias("pins").description("List workspace skills pinne
|
|
|
4619
4971
|
program.command("unpin <ref>").description("Unpin a workspace skill for local pull").option("--workspace <slug>", "Workspace slug").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").action((ref, opts) => unpinCommand(ref, opts));
|
|
4620
4972
|
program.command("share <ref> <email>").description("Invite someone to a skill by email.").option("--role <role>", "viewer (default) or editor").action((ref, email, opts) => shareCommand(ref, email, opts));
|
|
4621
4973
|
program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
|
|
4974
|
+
var linkCmd = program.command("link").description("Create opaque share links for unlisted/public skills");
|
|
4975
|
+
linkCmd.command("create <ref>").description("Create a share link URL for a skill.").option("--name <name>", "Optional link label").option("--role <role>", "viewer (default) or editor").action((ref, opts) => linkCreateCommand(ref, opts));
|
|
4622
4976
|
var configCmd = program.command("config").description("Manage local Floom configuration");
|
|
4623
4977
|
configCmd.command("default-workspace [slug]").description("Show or set the default workspace").option("--scope <scope>", "global | local", "local").action((slug, opts) => defaultWorkspaceCommand(slug, opts));
|
|
4624
4978
|
function addWorkspaceCommands(cmd) {
|
|
@@ -4677,4 +5031,3 @@ bcryptjs/dist/bcrypt.js:
|
|
|
4677
5031
|
* see: https://github.com/dcodeIO/bcrypt.js for details
|
|
4678
5032
|
*)
|
|
4679
5033
|
*/
|
|
4680
|
-
//# sourceMappingURL=index.js.map
|