@floomhq/skills 0.2.10 → 0.2.12
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 +314 -62
- package/dist/index.js.map +4 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2383,8 +2383,7 @@ var log = {
|
|
|
2383
2383
|
};
|
|
2384
2384
|
|
|
2385
2385
|
// src/commands/login.ts
|
|
2386
|
-
import {
|
|
2387
|
-
import { promisify as promisify2 } from "node:util";
|
|
2386
|
+
import { spawn } from "node:child_process";
|
|
2388
2387
|
|
|
2389
2388
|
// src/config.ts
|
|
2390
2389
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -2395,7 +2394,7 @@ var AUTH_FILE = join3(CONFIG_DIR, "auth.json");
|
|
|
2395
2394
|
var DEFAULT_APP_URL = "https://skills.floom.dev";
|
|
2396
2395
|
var DEFAULT_API_URL = "https://skills.floom.dev/api/v1";
|
|
2397
2396
|
var LEGACY_API_HOSTS = /* @__PURE__ */ new Set(["floom-v0.vercel.app", "skills.wasm.floom.dev"]);
|
|
2398
|
-
var TRUSTED_API_HOSTS = /* @__PURE__ */ new Set(["skills.floom.dev", "skills.wasm.floom.dev"
|
|
2397
|
+
var TRUSTED_API_HOSTS = /* @__PURE__ */ new Set(["skills.floom.dev", "skills.wasm.floom.dev"]);
|
|
2399
2398
|
async function ensureDir() {
|
|
2400
2399
|
await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
2401
2400
|
}
|
|
@@ -2484,19 +2483,33 @@ function isLegacyApiUrl(apiUrl) {
|
|
|
2484
2483
|
}
|
|
2485
2484
|
|
|
2486
2485
|
// src/version.ts
|
|
2487
|
-
var VERSION = "0.2.
|
|
2486
|
+
var VERSION = "0.2.12";
|
|
2488
2487
|
|
|
2489
2488
|
// src/api-client.ts
|
|
2490
2489
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
2490
|
+
var DEFAULT_RETRY_ATTEMPTS = 2;
|
|
2491
2491
|
function timeoutMs() {
|
|
2492
2492
|
const raw = Number(process.env.FLOOM_API_TIMEOUT_MS);
|
|
2493
2493
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
|
|
2494
2494
|
}
|
|
2495
|
+
function retryAttempts() {
|
|
2496
|
+
const raw = Number(process.env.FLOOM_API_RETRY_ATTEMPTS);
|
|
2497
|
+
return Number.isInteger(raw) && raw >= 0 ? Math.min(raw, 5) : DEFAULT_RETRY_ATTEMPTS;
|
|
2498
|
+
}
|
|
2499
|
+
function retryDelayMs(attempt) {
|
|
2500
|
+
const raw = Number(process.env.FLOOM_API_RETRY_DELAY_MS);
|
|
2501
|
+
const base = Number.isFinite(raw) && raw >= 0 ? raw : 150;
|
|
2502
|
+
return base * 2 ** attempt;
|
|
2503
|
+
}
|
|
2504
|
+
async function sleep(ms) {
|
|
2505
|
+
if (ms <= 0) return;
|
|
2506
|
+
await new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2507
|
+
}
|
|
2495
2508
|
async function fetchWithTimeout(url, init = {}) {
|
|
2496
2509
|
const controller = new AbortController();
|
|
2497
2510
|
const timer = setTimeout(() => controller.abort(), timeoutMs());
|
|
2498
2511
|
try {
|
|
2499
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
2512
|
+
return await fetch(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
2500
2513
|
} catch (e) {
|
|
2501
2514
|
if (e.name === "AbortError") {
|
|
2502
2515
|
throw new Error(`Request timed out after ${timeoutMs()}ms`);
|
|
@@ -2506,6 +2519,19 @@ async function fetchWithTimeout(url, init = {}) {
|
|
|
2506
2519
|
clearTimeout(timer);
|
|
2507
2520
|
}
|
|
2508
2521
|
}
|
|
2522
|
+
function redirectHost(res, requestUrl) {
|
|
2523
|
+
const location = res.headers.get("location");
|
|
2524
|
+
return location ? new URL(location, requestUrl).host : void 0;
|
|
2525
|
+
}
|
|
2526
|
+
function isRedirect(res) {
|
|
2527
|
+
return res.status >= 300 && res.status < 400;
|
|
2528
|
+
}
|
|
2529
|
+
function isRetryableStatus(status) {
|
|
2530
|
+
return status === 408 || status === 429 || status >= 500;
|
|
2531
|
+
}
|
|
2532
|
+
function isRetryableMethod(method) {
|
|
2533
|
+
return method === "GET" || method === "HEAD";
|
|
2534
|
+
}
|
|
2509
2535
|
async function api(path, opts = {}) {
|
|
2510
2536
|
const auth = await readAuth();
|
|
2511
2537
|
const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
|
|
@@ -2515,48 +2541,68 @@ async function api(path, opts = {}) {
|
|
|
2515
2541
|
let lastError = null;
|
|
2516
2542
|
const bases = getApiBaseUrls(auth?.apiUrl);
|
|
2517
2543
|
for (const base of bases) {
|
|
2518
|
-
const
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2544
|
+
const method = opts.method ?? "GET";
|
|
2545
|
+
const attempts = isRetryableMethod(method) ? retryAttempts() : 0;
|
|
2546
|
+
for (let attempt = 0; attempt <= attempts; attempt++) {
|
|
2547
|
+
const url = new URL(base + path);
|
|
2548
|
+
if (opts.query) {
|
|
2549
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
2550
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
2551
|
+
}
|
|
2522
2552
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
})
|
|
2538
|
-
|
|
2553
|
+
const headers = {
|
|
2554
|
+
"Content-Type": "application/json",
|
|
2555
|
+
"User-Agent": `floom-cli/${VERSION}`,
|
|
2556
|
+
"x-floom-cli-version": VERSION
|
|
2557
|
+
};
|
|
2558
|
+
const requestToken = opts.tokenOverride ?? token;
|
|
2559
|
+
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
|
|
2560
|
+
let res;
|
|
2561
|
+
try {
|
|
2562
|
+
res = await fetchWithTimeout(url.toString(), {
|
|
2563
|
+
method,
|
|
2564
|
+
headers,
|
|
2565
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
2566
|
+
});
|
|
2567
|
+
} catch (e) {
|
|
2568
|
+
lastError = new FloomError(
|
|
2569
|
+
"INTERNAL_ERROR",
|
|
2570
|
+
`Unable to reach Floom API at ${base}: ${e.message}`,
|
|
2571
|
+
{ apiUrl: base }
|
|
2572
|
+
);
|
|
2573
|
+
if (attempt < attempts) {
|
|
2574
|
+
await sleep(retryDelayMs(attempt));
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
break;
|
|
2578
|
+
}
|
|
2579
|
+
if (isRedirect(res)) {
|
|
2580
|
+
throw new FloomError(
|
|
2581
|
+
"INTERNAL_ERROR",
|
|
2582
|
+
"Floom API returned an unexpected redirect. Refusing to forward credentials.",
|
|
2583
|
+
{ status: res.status, apiUrl: base, redirectHost: redirectHost(res, url) }
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
const text = await res.text();
|
|
2587
|
+
let json = null;
|
|
2588
|
+
try {
|
|
2589
|
+
json = text ? JSON.parse(text) : null;
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
if (res.ok) return json;
|
|
2593
|
+
const err = json?.error ?? {};
|
|
2539
2594
|
lastError = new FloomError(
|
|
2540
|
-
"INTERNAL_ERROR",
|
|
2541
|
-
|
|
2542
|
-
{ apiUrl: base }
|
|
2595
|
+
err.code ?? "INTERNAL_ERROR",
|
|
2596
|
+
err.message ?? `HTTP ${res.status} ${res.statusText}`,
|
|
2597
|
+
{ status: res.status, requestId: err.request_id, apiUrl: base }
|
|
2543
2598
|
);
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
} catch {
|
|
2599
|
+
if (isRetryableStatus(res.status) && attempt < attempts) {
|
|
2600
|
+
await sleep(retryDelayMs(attempt));
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (res.status !== 404 || json?.error) break;
|
|
2604
|
+
break;
|
|
2551
2605
|
}
|
|
2552
|
-
if (res.ok) return json;
|
|
2553
|
-
const err = json?.error ?? {};
|
|
2554
|
-
lastError = new FloomError(
|
|
2555
|
-
err.code ?? "INTERNAL_ERROR",
|
|
2556
|
-
err.message ?? `HTTP ${res.status} ${res.statusText}`,
|
|
2557
|
-
{ status: res.status, requestId: err.request_id, apiUrl: base }
|
|
2558
|
-
);
|
|
2559
|
-
if (res.status !== 404 || json?.error) break;
|
|
2560
2606
|
}
|
|
2561
2607
|
throw lastError ?? new FloomError("INTERNAL_ERROR", "API request failed");
|
|
2562
2608
|
}
|
|
@@ -2572,6 +2618,12 @@ async function rawPut(url, body, contentType = "application/octet-stream") {
|
|
|
2572
2618
|
} catch (e) {
|
|
2573
2619
|
throw new FloomError("UPLOAD_FAILED", `Upload failed: ${e.message}`);
|
|
2574
2620
|
}
|
|
2621
|
+
if (isRedirect(res)) {
|
|
2622
|
+
throw new FloomError(
|
|
2623
|
+
"UPLOAD_FAILED",
|
|
2624
|
+
`Upload failed: unexpected redirect to ${redirectHost(res, new URL(url)) ?? "unknown"}`
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2575
2627
|
if (!res.ok) {
|
|
2576
2628
|
throw new FloomError(
|
|
2577
2629
|
"UPLOAD_FAILED",
|
|
@@ -2586,6 +2638,12 @@ async function rawGet(url) {
|
|
|
2586
2638
|
} catch (e) {
|
|
2587
2639
|
throw new FloomError("DOWNLOAD_FAILED", `Download failed: ${e.message}`);
|
|
2588
2640
|
}
|
|
2641
|
+
if (isRedirect(res)) {
|
|
2642
|
+
throw new FloomError(
|
|
2643
|
+
"DOWNLOAD_FAILED",
|
|
2644
|
+
`Download failed: unexpected redirect to ${redirectHost(res, new URL(url)) ?? "unknown"}`
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2589
2647
|
if (!res.ok) {
|
|
2590
2648
|
throw new FloomError(
|
|
2591
2649
|
"DOWNLOAD_FAILED",
|
|
@@ -2597,11 +2655,14 @@ async function rawGet(url) {
|
|
|
2597
2655
|
}
|
|
2598
2656
|
|
|
2599
2657
|
// src/commands/login.ts
|
|
2600
|
-
var sh = promisify2(exec);
|
|
2601
2658
|
async function openInBrowser(url) {
|
|
2602
|
-
const
|
|
2659
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2660
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2603
2661
|
try {
|
|
2604
|
-
|
|
2662
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", shell: false });
|
|
2663
|
+
child.on("error", () => {
|
|
2664
|
+
});
|
|
2665
|
+
child.unref();
|
|
2605
2666
|
} catch {
|
|
2606
2667
|
}
|
|
2607
2668
|
}
|
|
@@ -2615,7 +2676,7 @@ async function loginCommand() {
|
|
|
2615
2676
|
log.info(`Open this URL in your browser:`);
|
|
2616
2677
|
log.info(` ${session.verification_uri}`);
|
|
2617
2678
|
log.info("");
|
|
2618
|
-
log.info(`
|
|
2679
|
+
log.info(`Confirm this code in the browser: ${session.user_code}`);
|
|
2619
2680
|
log.info("");
|
|
2620
2681
|
log.info("Waiting for approval in the browser. Press Ctrl+C to cancel.");
|
|
2621
2682
|
openInBrowser(session.verification_uri).catch(() => {
|
|
@@ -2684,7 +2745,13 @@ async function whoamiCommand() {
|
|
|
2684
2745
|
try {
|
|
2685
2746
|
me = await api("/me", { authRequired: true });
|
|
2686
2747
|
} catch (e) {
|
|
2687
|
-
|
|
2748
|
+
if (e instanceof FloomError && e.code === "TOKEN_EXPIRED") {
|
|
2749
|
+
log.err("Login expired.");
|
|
2750
|
+
} else if (e instanceof FloomError && e.code === "TOKEN_INVALID") {
|
|
2751
|
+
log.err("Login revoked or unknown.");
|
|
2752
|
+
} else {
|
|
2753
|
+
log.err(`Login is not accepted by the Floom API: ${e.message}`);
|
|
2754
|
+
}
|
|
2688
2755
|
log.info("Run: floom login");
|
|
2689
2756
|
process.exitCode = 1;
|
|
2690
2757
|
return;
|
|
@@ -3364,8 +3431,12 @@ async function updateCommand(refStr, opts = {}) {
|
|
|
3364
3431
|
async function listCommand(opts = {}) {
|
|
3365
3432
|
const resp = await api("/skills", {
|
|
3366
3433
|
authRequired: true,
|
|
3367
|
-
query: { q: opts.query, library: opts.workspace ?? opts.library, folder: opts.folder }
|
|
3434
|
+
query: { q: opts.query, library: opts.workspace ?? opts.library, folder: opts.folder, tag: opts.tag }
|
|
3368
3435
|
});
|
|
3436
|
+
if (opts.json) {
|
|
3437
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3369
3440
|
if (resp.skills.length === 0) {
|
|
3370
3441
|
log.info("No skills.");
|
|
3371
3442
|
return;
|
|
@@ -3386,7 +3457,8 @@ async function listCommand(opts = {}) {
|
|
|
3386
3457
|
for (const [library, rows] of groups.entries()) {
|
|
3387
3458
|
log.heading(library);
|
|
3388
3459
|
for (const s of rows) {
|
|
3389
|
-
|
|
3460
|
+
const tagText = s.tags?.length ? ` #${s.tags.join(" #")}` : "";
|
|
3461
|
+
console.log(` ${`${s.library.slug}/${s.slug}`.padEnd(40)} ${s.visibility.padEnd(10)} ${s.title}${tagText}`);
|
|
3390
3462
|
}
|
|
3391
3463
|
}
|
|
3392
3464
|
}
|
|
@@ -3414,12 +3486,19 @@ async function infoCommand(refStr) {
|
|
|
3414
3486
|
}
|
|
3415
3487
|
|
|
3416
3488
|
// src/commands/share.ts
|
|
3489
|
+
function isValidEmail(email) {
|
|
3490
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
3491
|
+
}
|
|
3417
3492
|
async function shareCommand(refStr, email, opts = {}) {
|
|
3418
3493
|
const ref = parseSkillRef(refStr);
|
|
3419
3494
|
if (!ref) {
|
|
3420
3495
|
log.err(`Invalid skill ref: ${refStr}`);
|
|
3421
3496
|
process.exit(1);
|
|
3422
3497
|
}
|
|
3498
|
+
if (!isValidEmail(email)) {
|
|
3499
|
+
log.err(`Invalid email: ${email}`);
|
|
3500
|
+
process.exit(1);
|
|
3501
|
+
}
|
|
3423
3502
|
const role = opts.role ?? "viewer";
|
|
3424
3503
|
const r = await api(`/skills/${ref.owner}/${ref.slug}/grants`, {
|
|
3425
3504
|
method: "POST",
|
|
@@ -3435,6 +3514,10 @@ async function unshareCommand(refStr, email) {
|
|
|
3435
3514
|
log.err(`Invalid skill ref: ${refStr}`);
|
|
3436
3515
|
process.exit(1);
|
|
3437
3516
|
}
|
|
3517
|
+
if (!isValidEmail(email)) {
|
|
3518
|
+
log.err(`Invalid email: ${email}`);
|
|
3519
|
+
process.exit(1);
|
|
3520
|
+
}
|
|
3438
3521
|
const list = await api(`/skills/${ref.owner}/${ref.slug}/grants`, { authRequired: true });
|
|
3439
3522
|
const grant = list.grants.find((g) => (g.email ?? "").toLowerCase() === email.toLowerCase());
|
|
3440
3523
|
if (!grant) {
|
|
@@ -3446,6 +3529,9 @@ async function unshareCommand(refStr, email) {
|
|
|
3446
3529
|
}
|
|
3447
3530
|
|
|
3448
3531
|
// src/commands/library.ts
|
|
3532
|
+
function isValidEmail2(email) {
|
|
3533
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
3534
|
+
}
|
|
3449
3535
|
async function libraryListCommand() {
|
|
3450
3536
|
const resp = await api("/libraries", { authRequired: true });
|
|
3451
3537
|
if (!resp.libraries.length) {
|
|
@@ -3461,17 +3547,52 @@ async function libraryCreateCommand(slug, name) {
|
|
|
3461
3547
|
log.ok(`Created workspace ${resp.slug}`);
|
|
3462
3548
|
}
|
|
3463
3549
|
async function libraryInviteCommand(librarySlug, email, role = "viewer") {
|
|
3464
|
-
|
|
3550
|
+
if (!isValidEmail2(email)) {
|
|
3551
|
+
log.err(`Invalid email: ${email}`);
|
|
3552
|
+
process.exit(1);
|
|
3553
|
+
}
|
|
3554
|
+
const resp = await api(`/libraries/${librarySlug}/pending-invites`, {
|
|
3465
3555
|
method: "POST",
|
|
3466
3556
|
authRequired: true,
|
|
3467
3557
|
body: { email, role }
|
|
3468
3558
|
});
|
|
3469
3559
|
log.ok(`Invited ${email} to ${librarySlug} as ${role}`);
|
|
3560
|
+
if (resp.invite_url) log.info(`Invite link: ${resp.invite_url}`);
|
|
3470
3561
|
}
|
|
3471
3562
|
async function libraryLeaveCommand(librarySlug) {
|
|
3472
3563
|
await api(`/libraries/${librarySlug}/members/me`, { method: "DELETE", authRequired: true });
|
|
3473
3564
|
log.ok(`Left workspace ${librarySlug}`);
|
|
3474
3565
|
}
|
|
3566
|
+
async function workspaceInvitesCommand() {
|
|
3567
|
+
const resp = await api("/me/invites", { authRequired: true });
|
|
3568
|
+
if (!resp.invites.length) {
|
|
3569
|
+
log.info("No pending workspace invites.");
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
for (const invite of resp.invites) {
|
|
3573
|
+
const workspace = invite.workspace?.slug ?? "(missing workspace)";
|
|
3574
|
+
const inviter = invite.inviter.handle ? `@${invite.inviter.handle}` : "admin";
|
|
3575
|
+
console.log(`${workspace.padEnd(24)} ${invite.role.padEnd(6)} ${invite.status.padEnd(8)} from ${inviter} expires ${invite.expires_at}`);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
function inviteToken(input) {
|
|
3579
|
+
try {
|
|
3580
|
+
const url = new URL(input);
|
|
3581
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
3582
|
+
const inviteIndex = parts.indexOf("invite");
|
|
3583
|
+
if (inviteIndex >= 0 && parts[inviteIndex + 1]) return parts[inviteIndex + 1];
|
|
3584
|
+
} catch {
|
|
3585
|
+
}
|
|
3586
|
+
return input.trim();
|
|
3587
|
+
}
|
|
3588
|
+
async function workspaceAcceptCommand(tokenOrUrl) {
|
|
3589
|
+
const token = inviteToken(tokenOrUrl);
|
|
3590
|
+
const resp = await api(`/invites/${encodeURIComponent(token)}/accept`, {
|
|
3591
|
+
method: "POST",
|
|
3592
|
+
authRequired: true
|
|
3593
|
+
});
|
|
3594
|
+
log.ok(`Workspace invite ${resp.status}${resp.role ? ` as ${resp.role}` : ""}`);
|
|
3595
|
+
}
|
|
3475
3596
|
|
|
3476
3597
|
// src/lib/config-file.ts
|
|
3477
3598
|
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
|
|
@@ -3888,18 +4009,33 @@ async function statusCommand(opts = {}) {
|
|
|
3888
4009
|
const scope = normalizeScope(opts.scope);
|
|
3889
4010
|
const config = await readFloomConfig(scope);
|
|
3890
4011
|
const active = config.targets[target]?.active_workspaces ?? [];
|
|
3891
|
-
|
|
3892
|
-
log.kv("Default workspace", config.default_workspace ?? "(none)");
|
|
3893
|
-
log.kv("Active workspaces", active.length ? active.join(", ") : "(none)");
|
|
3894
|
-
log.kv("Local pull policy", "router + pinned skills + instructions only");
|
|
4012
|
+
const workspaces = [];
|
|
3895
4013
|
for (const workspace of active) {
|
|
3896
4014
|
const pins = await api(`/libraries/${workspace}/pins`, {
|
|
3897
4015
|
authRequired: true,
|
|
3898
4016
|
query: { target }
|
|
3899
4017
|
});
|
|
4018
|
+
workspaces.push({ workspace, pins: pins.pins });
|
|
4019
|
+
}
|
|
4020
|
+
if (opts.json) {
|
|
4021
|
+
console.log(JSON.stringify({
|
|
4022
|
+
target,
|
|
4023
|
+
scope,
|
|
4024
|
+
default_workspace: config.default_workspace ?? null,
|
|
4025
|
+
active_workspaces: active,
|
|
4026
|
+
pull_policy: "router + pinned skills + instructions only",
|
|
4027
|
+
workspaces
|
|
4028
|
+
}, null, 2));
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
log.heading(`Floom status for ${target} (${scope})`);
|
|
4032
|
+
log.kv("Default workspace", config.default_workspace ?? "(none)");
|
|
4033
|
+
log.kv("Active workspaces", active.length ? active.join(", ") : "(none)");
|
|
4034
|
+
log.kv("Local pull policy", "router + pinned skills + instructions only");
|
|
4035
|
+
for (const workspace of workspaces) {
|
|
3900
4036
|
log.blank();
|
|
3901
|
-
log.info(`${workspace}: ${
|
|
3902
|
-
for (const pin of
|
|
4037
|
+
log.info(`${workspace.workspace}: ${workspace.pins.length} pinned skill(s) for ${target}`);
|
|
4038
|
+
for (const pin of workspace.pins) {
|
|
3903
4039
|
log.kv("", `${pin.skill?.slug ?? pin.skill_id}${pin.skill?.latest?.version ? `@${pin.skill.latest.version}` : ""}`);
|
|
3904
4040
|
}
|
|
3905
4041
|
}
|
|
@@ -3960,6 +4096,29 @@ async function pinCommand(ref, opts = {}) {
|
|
|
3960
4096
|
});
|
|
3961
4097
|
log.ok(`Pinned ${ref} for ${target} in ${workspace}`);
|
|
3962
4098
|
}
|
|
4099
|
+
async function pinnedCommand(opts = {}) {
|
|
4100
|
+
const target = assertTarget(opts.target ?? "codex");
|
|
4101
|
+
const workspace = await resolveWorkspace(opts);
|
|
4102
|
+
const resp = await api(`/libraries/${workspace}/pins`, {
|
|
4103
|
+
authRequired: true,
|
|
4104
|
+
query: { target }
|
|
4105
|
+
});
|
|
4106
|
+
if (opts.json) {
|
|
4107
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
if (resp.pins.length === 0) {
|
|
4111
|
+
log.info(`No pinned skills for ${target} in ${workspace}.`);
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
log.heading(`Pinned skills for ${target} in ${workspace}`);
|
|
4115
|
+
for (const pin of resp.pins) {
|
|
4116
|
+
const ref = pin.skill?.slug ? `${workspace}/${pin.skill.slug}` : pin.skill_id;
|
|
4117
|
+
const version = pin.skill?.latest?.version ? `@${pin.skill.latest.version}` : "";
|
|
4118
|
+
const title = pin.skill?.title ? ` ${pin.skill.title}` : "";
|
|
4119
|
+
console.log(` ${`${ref}${version}`.padEnd(42)} ${pin.created_at}${title}`);
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
3963
4122
|
async function unpinCommand(ref, opts = {}) {
|
|
3964
4123
|
const target = assertTarget(opts.target ?? "codex");
|
|
3965
4124
|
const workspace = await resolveWorkspace(opts);
|
|
@@ -3971,6 +4130,78 @@ async function unpinCommand(ref, opts = {}) {
|
|
|
3971
4130
|
log.ok(`Unpinned ${ref} for ${target} in ${workspace}`);
|
|
3972
4131
|
}
|
|
3973
4132
|
|
|
4133
|
+
// src/commands/folder.ts
|
|
4134
|
+
function slugify(value) {
|
|
4135
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
4136
|
+
}
|
|
4137
|
+
async function folderListCommand(workspace) {
|
|
4138
|
+
const resp = await api(`/libraries/${encodeURIComponent(workspace)}/folders`, { authRequired: true });
|
|
4139
|
+
if (resp.folders.length === 0) {
|
|
4140
|
+
log.info("No folders.");
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
for (const folder of resp.folders) {
|
|
4144
|
+
console.log(`${folder.id} ${folder.slug.padEnd(24)} ${folder.access_mode.padEnd(10)} ${String(folder.skills_count ?? 0).padStart(3)} skills ${folder.name}`);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
async function folderCreateCommand(workspace, name, opts = {}) {
|
|
4148
|
+
const slug = opts.slug ?? slugify(name);
|
|
4149
|
+
const folder = await api(`/libraries/${encodeURIComponent(workspace)}/folders`, {
|
|
4150
|
+
method: "POST",
|
|
4151
|
+
authRequired: true,
|
|
4152
|
+
body: {
|
|
4153
|
+
name,
|
|
4154
|
+
slug,
|
|
4155
|
+
access_mode: opts.access ?? "open",
|
|
4156
|
+
public_release_policy: opts.publicRelease ?? "allowed",
|
|
4157
|
+
default_visibility: opts.visibility ?? "private"
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4160
|
+
log.ok(`Created folder ${folder.slug} in ${workspace}.`);
|
|
4161
|
+
log.kv("Folder ID", folder.id);
|
|
4162
|
+
}
|
|
4163
|
+
async function folderArchiveCommand(workspace, folderId) {
|
|
4164
|
+
await api(`/libraries/${encodeURIComponent(workspace)}/folders/${encodeURIComponent(folderId)}`, {
|
|
4165
|
+
method: "DELETE",
|
|
4166
|
+
authRequired: true
|
|
4167
|
+
});
|
|
4168
|
+
log.ok(`Archived folder ${folderId}.`);
|
|
4169
|
+
}
|
|
4170
|
+
async function folderMoveCommand(ref, folderId, opts = {}) {
|
|
4171
|
+
const cleaned = ref.replace(/^@/, "");
|
|
4172
|
+
const parts = cleaned.split("/");
|
|
4173
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
4174
|
+
throw new Error("Expected ref format: workspace/skill or @workspace/skill");
|
|
4175
|
+
}
|
|
4176
|
+
const [owner, slug] = parts;
|
|
4177
|
+
const targetFolderId = opts.root ? null : folderId;
|
|
4178
|
+
if (!opts.root && !targetFolderId) throw new Error("Provide a folder id, or pass --root to move to workspace root.");
|
|
4179
|
+
const resp = await api(`/skills/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}/move`, {
|
|
4180
|
+
method: "POST",
|
|
4181
|
+
authRequired: true,
|
|
4182
|
+
body: {
|
|
4183
|
+
target_library_slug: opts.workspace,
|
|
4184
|
+
target_folder_id: targetFolderId
|
|
4185
|
+
}
|
|
4186
|
+
});
|
|
4187
|
+
log.ok(`Moved ${ref} to ${resp.folder_id ? `folder ${resp.folder_id}` : "workspace root"} in ${resp.library_slug}.`);
|
|
4188
|
+
}
|
|
4189
|
+
async function folderGrantCommand(workspace, folderId, email, role) {
|
|
4190
|
+
await api(`/libraries/${encodeURIComponent(workspace)}/folders/${encodeURIComponent(folderId)}/members`, {
|
|
4191
|
+
method: "POST",
|
|
4192
|
+
authRequired: true,
|
|
4193
|
+
body: { email, role }
|
|
4194
|
+
});
|
|
4195
|
+
log.ok(`Granted ${role} folder access to ${email}.`);
|
|
4196
|
+
}
|
|
4197
|
+
async function folderRevokeCommand(workspace, folderId, memberId) {
|
|
4198
|
+
await api(`/libraries/${encodeURIComponent(workspace)}/folders/${encodeURIComponent(folderId)}/members/${encodeURIComponent(memberId)}`, {
|
|
4199
|
+
method: "DELETE",
|
|
4200
|
+
authRequired: true
|
|
4201
|
+
});
|
|
4202
|
+
log.ok(`Revoked folder member ${memberId}.`);
|
|
4203
|
+
}
|
|
4204
|
+
|
|
3974
4205
|
// src/commands/mcp.ts
|
|
3975
4206
|
import { mkdtemp, mkdir as mkdir9, readdir as readdir4, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
|
|
3976
4207
|
import { join as join13 } from "node:path";
|
|
@@ -3992,7 +4223,7 @@ async function fetchWithTimeout2(url, init = {}) {
|
|
|
3992
4223
|
const controller = new AbortController();
|
|
3993
4224
|
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
3994
4225
|
try {
|
|
3995
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
4226
|
+
return await fetch(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
3996
4227
|
} catch (e) {
|
|
3997
4228
|
if (e.name === "AbortError") throw new Error(`Request timed out after ${API_TIMEOUT_MS}ms`);
|
|
3998
4229
|
throw e;
|
|
@@ -4032,6 +4263,12 @@ async function apiRequest(token, path, query) {
|
|
|
4032
4263
|
lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
|
|
4033
4264
|
continue;
|
|
4034
4265
|
}
|
|
4266
|
+
if (res.status >= 300 && res.status < 400) {
|
|
4267
|
+
const location = res.headers.get("location");
|
|
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.`);
|
|
4270
|
+
break;
|
|
4271
|
+
}
|
|
4035
4272
|
const body = await res.text();
|
|
4036
4273
|
let json = null;
|
|
4037
4274
|
try {
|
|
@@ -4373,11 +4610,12 @@ program.command("install <ref>").description("Install a skill (default: .agents/
|
|
|
4373
4610
|
program.command("installed").description("List installed skills in this project.").option("--json").action(installedCommand);
|
|
4374
4611
|
program.command("outdated").description("Show installed skills with newer versions available.").action(outdatedCommand);
|
|
4375
4612
|
program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
|
|
4376
|
-
program.command("list").description("List remote skills.").option("--mine", "Only your own skills (default)").option("--workspace <slug>", "Filter by workspace slug").option("--library <slug>", "Legacy alias for --workspace").option("--folder <uuid>", "Filter by folder id").option("--flat", "Print one ref per line").option("--query <q>", "Filter by query").action(listCommand);
|
|
4613
|
+
program.command("list").description("List remote skills.").option("--mine", "Only your own skills (default)").option("--workspace <slug>", "Filter by workspace slug").option("--library <slug>", "Legacy alias for --workspace").option("--folder <uuid>", "Filter by folder id").option("--tag <tag>", "Filter by tag").option("--flat", "Print one ref per line").option("--json", "Emit machine-readable JSON").option("--query <q>", "Filter by query").action(listCommand);
|
|
4377
4614
|
program.command("info <ref>").description("Show details for a remote skill.").action(infoCommand);
|
|
4378
|
-
program.command("status").description("Show local vs remote Floom workspace state").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((opts) => statusCommand(opts));
|
|
4615
|
+
program.command("status").description("Show local vs remote Floom workspace state").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").option("--json", "Emit machine-readable JSON").action((opts) => statusCommand(opts));
|
|
4379
4616
|
program.command("pull").description("Pull account/workspace instructions for active workspaces").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").option("--apply", "Append managed block when the target file has no Floom block").option("--force", "Write without the first-apply guard").action((opts) => pullCommand(opts));
|
|
4380
4617
|
program.command("pin <ref>").description("Pin a workspace skill for local pull").option("--workspace <slug>", "Workspace slug").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").action((ref, opts) => pinCommand(ref, opts));
|
|
4618
|
+
program.command("pinned").alias("pins").description("List workspace skills pinned for local pull").option("--workspace <slug>", "Workspace slug").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--json", "Emit machine-readable JSON").action((opts) => pinnedCommand(opts));
|
|
4381
4619
|
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));
|
|
4382
4620
|
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));
|
|
4383
4621
|
program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
|
|
@@ -4387,6 +4625,8 @@ function addWorkspaceCommands(cmd) {
|
|
|
4387
4625
|
cmd.command("list").action(libraryListCommand);
|
|
4388
4626
|
cmd.command("create <slug> <name>").action((slug, name) => libraryCreateCommand(slug, name));
|
|
4389
4627
|
cmd.command("invite <workspaceSlug> <email>").option("--role <role>", "viewer|editor|admin", "viewer").action((workspaceSlug, email, opts) => libraryInviteCommand(workspaceSlug, email, opts.role));
|
|
4628
|
+
cmd.command("invites").description("List pending workspace invites for the logged-in user").action(workspaceInvitesCommand);
|
|
4629
|
+
cmd.command("accept <tokenOrUrl>").description("Accept a workspace invite token or URL").action((tokenOrUrl) => workspaceAcceptCommand(tokenOrUrl));
|
|
4390
4630
|
cmd.command("leave <workspaceSlug>").action((workspaceSlug) => libraryLeaveCommand(workspaceSlug));
|
|
4391
4631
|
cmd.command("activate <workspaceSlug>").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((workspaceSlug, opts) => workspaceActivateCommand(workspaceSlug, opts));
|
|
4392
4632
|
cmd.command("deactivate <workspaceSlug>").option("--target <target>", "claude | codex | cursor | kimi | opencode", "codex").option("--scope <scope>", "global | local", "local").action((workspaceSlug, opts) => workspaceDeactivateCommand(workspaceSlug, opts));
|
|
@@ -4394,6 +4634,13 @@ function addWorkspaceCommands(cmd) {
|
|
|
4394
4634
|
}
|
|
4395
4635
|
addWorkspaceCommands(program.command("workspace").description("Manage workspaces"));
|
|
4396
4636
|
addWorkspaceCommands(program.command("library").description("Manage workspaces (legacy alias)"));
|
|
4637
|
+
var folderCmd = program.command("folder").description("Manage workspace folders");
|
|
4638
|
+
folderCmd.command("list <workspace>").description("List folders in a workspace").action((workspace) => folderListCommand(workspace));
|
|
4639
|
+
folderCmd.command("create <workspace> <name>").description("Create a flat folder in a workspace").option("--slug <slug>", "Folder slug").option("--access <mode>", "open | restricted", "open").option("--public-release <policy>", "allowed | review_required | blocked", "allowed").option("--visibility <visibility>", "private | unlisted | public", "private").action((workspace, name, opts) => folderCreateCommand(workspace, name, opts));
|
|
4640
|
+
folderCmd.command("archive <workspace> <folderId>").description("Archive an empty folder").action((workspace, folderId) => folderArchiveCommand(workspace, folderId));
|
|
4641
|
+
folderCmd.command("move <ref> [folderId]").description("Move a skill into a folder or back to workspace root").option("--workspace <slug>", "Move to another workspace").option("--root", "Move to workspace root").action((ref, folderId, opts) => folderMoveCommand(ref, folderId, opts));
|
|
4642
|
+
folderCmd.command("grant <workspace> <folderId> <email>").description("Grant an existing workspace member access to a restricted folder").option("--role <role>", "viewer | editor | admin", "viewer").action((workspace, folderId, email, opts) => folderGrantCommand(workspace, folderId, email, opts.role));
|
|
4643
|
+
folderCmd.command("revoke <workspace> <folderId> <memberId>").description("Revoke a folder member grant").action((workspace, folderId, memberId) => folderRevokeCommand(workspace, folderId, memberId));
|
|
4397
4644
|
var instructionCmd = program.command("instruction").description("Manage account and workspace instructions");
|
|
4398
4645
|
instructionCmd.command("pull").description("Pull an account or workspace instruction into a managed local block").option("--account", "Pull account instruction").option("--workspace <slug>", "Pull workspace instruction").option("--target <target>", "default | claude | codex | cursor | opencode", "codex").option("--scope <scope>", "global | local", "local").option("--path <path>", "Instruction file path override").option("--apply", "Append managed block when the target file has no Floom block").option("--force", "Write without the first-apply guard").action((opts) => instructionPullCommand(opts));
|
|
4399
4646
|
instructionCmd.command("push <file>").description("Publish an account or workspace instruction from a markdown file").option("--account", "Publish account instruction").option("--workspace <slug>", "Publish workspace instruction").option("--target <target>", "default | claude | codex | cursor | opencode", "default").option("--changelog <text>", "Version changelog").action((file, opts) => instructionPushCommand(file, opts));
|
|
@@ -4410,8 +4657,13 @@ async function main() {
|
|
|
4410
4657
|
}
|
|
4411
4658
|
process.exit(1);
|
|
4412
4659
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4660
|
+
const error = e;
|
|
4661
|
+
log.err(error.message ?? "Unknown error");
|
|
4662
|
+
if (process.env.FLOOM_DEBUG) {
|
|
4663
|
+
const name = error.name || "Error";
|
|
4664
|
+
const code = error.code ? ` code=${error.code}` : "";
|
|
4665
|
+
console.error(`[debug] ${name}${code}`);
|
|
4666
|
+
}
|
|
4415
4667
|
process.exit(1);
|
|
4416
4668
|
}
|
|
4417
4669
|
}
|