@floomhq/skills 0.2.11 → 0.2.13

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 CHANGED
@@ -2383,8 +2383,7 @@ var log = {
2383
2383
  };
2384
2384
 
2385
2385
  // src/commands/login.ts
2386
- import { exec } from "node:child_process";
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";
@@ -2484,19 +2483,33 @@ function isLegacyApiUrl(apiUrl) {
2484
2483
  }
2485
2484
 
2486
2485
  // src/version.ts
2487
- var VERSION = "0.2.11";
2486
+ var VERSION = "0.2.13";
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 url = new URL(base + path);
2519
- if (opts.query) {
2520
- for (const [k, v] of Object.entries(opts.query)) {
2521
- if (v !== void 0) url.searchParams.set(k, String(v));
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
- const headers = {
2525
- "Content-Type": "application/json",
2526
- "User-Agent": `floom-cli/${VERSION}`,
2527
- "x-floom-cli-version": VERSION
2528
- };
2529
- const requestToken = opts.tokenOverride ?? token;
2530
- if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
2531
- let res;
2532
- try {
2533
- res = await fetchWithTimeout(url.toString(), {
2534
- method: opts.method ?? "GET",
2535
- headers,
2536
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
2537
- });
2538
- } catch (e) {
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
- `Unable to reach Floom API at ${base}: ${e.message}`,
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
- continue;
2545
- }
2546
- const text = await res.text();
2547
- let json = null;
2548
- try {
2549
- json = text ? JSON.parse(text) : null;
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 opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
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
- await sh(`${opener} ${JSON.stringify(url)}`);
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
  }
@@ -2918,6 +2979,15 @@ async function validateCommand(opts = {}) {
2918
2979
  // src/commands/publish.ts
2919
2980
  import { readFile as readFile6 } from "node:fs/promises";
2920
2981
  import { join as join6 } from "node:path";
2982
+ function buildAuthenticatedSkillUrl(appUrl, librarySlug, skillSlug) {
2983
+ const base = appUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
2984
+ return `${base}/library/${encodeURIComponent(skillSlug)}?lib=${encodeURIComponent(librarySlug)}`;
2985
+ }
2986
+ function buildPublicSkillUrl(appUrl, handle, librarySlug, skillSlug) {
2987
+ const base = appUrl.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
2988
+ if (librarySlug !== handle) return null;
2989
+ return `${base}/@${handle}/${skillSlug}`;
2990
+ }
2921
2991
  async function publishCommand(opts = {}) {
2922
2992
  const auth = await readAuth();
2923
2993
  const envToken = process.env.FLOOM_API_TOKEN?.trim();
@@ -3000,9 +3070,17 @@ async function publishCommand(opts = {}) {
3000
3070
  log.blank();
3001
3071
  log.ok(`Published ${complete.ref}`);
3002
3072
  log.blank();
3003
- log.info("View:");
3073
+ log.info("Manage:");
3004
3074
  const displayApiUrl = trustedApiUrlOrDefault(process.env.FLOOM_API_URL ?? auth?.apiUrl ?? DEFAULT_API_URL);
3005
- log.kv("", `${displayApiUrl.replace("/api/v1", "")}/@${handle}/${manifest.name}`);
3075
+ log.kv("", buildAuthenticatedSkillUrl(displayApiUrl, refRoot, manifest.name));
3076
+ const publicUrl = buildPublicSkillUrl(displayApiUrl, handle, refRoot, manifest.name);
3077
+ if (publicUrl) {
3078
+ log.info("Public URL after you make it public:");
3079
+ log.kv("", publicUrl);
3080
+ } else {
3081
+ log.info("Public profile URL:");
3082
+ log.kv("", "Workspace skills stay under their authenticated library URL unless copied to your personal library.");
3083
+ }
3006
3084
  log.info("Install:");
3007
3085
  log.kv("", complete.install_command);
3008
3086
  }
@@ -3370,8 +3448,12 @@ async function updateCommand(refStr, opts = {}) {
3370
3448
  async function listCommand(opts = {}) {
3371
3449
  const resp = await api("/skills", {
3372
3450
  authRequired: true,
3373
- query: { q: opts.query, library: opts.workspace ?? opts.library, folder: opts.folder }
3451
+ query: { q: opts.query, library: opts.workspace ?? opts.library, folder: opts.folder, tag: opts.tag }
3374
3452
  });
3453
+ if (opts.json) {
3454
+ console.log(JSON.stringify(resp, null, 2));
3455
+ return;
3456
+ }
3375
3457
  if (resp.skills.length === 0) {
3376
3458
  log.info("No skills.");
3377
3459
  return;
@@ -3392,7 +3474,8 @@ async function listCommand(opts = {}) {
3392
3474
  for (const [library, rows] of groups.entries()) {
3393
3475
  log.heading(library);
3394
3476
  for (const s of rows) {
3395
- console.log(` ${`${s.library.slug}/${s.slug}`.padEnd(40)} ${s.visibility.padEnd(10)} ${s.title}`);
3477
+ const tagText = s.tags?.length ? ` #${s.tags.join(" #")}` : "";
3478
+ console.log(` ${`${s.library.slug}/${s.slug}`.padEnd(40)} ${s.visibility.padEnd(10)} ${s.title}${tagText}`);
3396
3479
  }
3397
3480
  }
3398
3481
  }
@@ -3485,17 +3568,48 @@ async function libraryInviteCommand(librarySlug, email, role = "viewer") {
3485
3568
  log.err(`Invalid email: ${email}`);
3486
3569
  process.exit(1);
3487
3570
  }
3488
- await api(`/libraries/${librarySlug}/pending-invites`, {
3571
+ const resp = await api(`/libraries/${librarySlug}/pending-invites`, {
3489
3572
  method: "POST",
3490
3573
  authRequired: true,
3491
3574
  body: { email, role }
3492
3575
  });
3493
3576
  log.ok(`Invited ${email} to ${librarySlug} as ${role}`);
3577
+ if (resp.invite_url) log.info(`Invite link: ${resp.invite_url}`);
3494
3578
  }
3495
3579
  async function libraryLeaveCommand(librarySlug) {
3496
3580
  await api(`/libraries/${librarySlug}/members/me`, { method: "DELETE", authRequired: true });
3497
3581
  log.ok(`Left workspace ${librarySlug}`);
3498
3582
  }
3583
+ async function workspaceInvitesCommand() {
3584
+ const resp = await api("/me/invites", { authRequired: true });
3585
+ if (!resp.invites.length) {
3586
+ log.info("No pending workspace invites.");
3587
+ return;
3588
+ }
3589
+ for (const invite of resp.invites) {
3590
+ const workspace = invite.workspace?.slug ?? "(missing workspace)";
3591
+ const inviter = invite.inviter.handle ? `@${invite.inviter.handle}` : "admin";
3592
+ console.log(`${workspace.padEnd(24)} ${invite.role.padEnd(6)} ${invite.status.padEnd(8)} from ${inviter} expires ${invite.expires_at}`);
3593
+ }
3594
+ }
3595
+ function inviteToken(input) {
3596
+ try {
3597
+ const url = new URL(input);
3598
+ const parts = url.pathname.split("/").filter(Boolean);
3599
+ const inviteIndex = parts.indexOf("invite");
3600
+ if (inviteIndex >= 0 && parts[inviteIndex + 1]) return parts[inviteIndex + 1];
3601
+ } catch {
3602
+ }
3603
+ return input.trim();
3604
+ }
3605
+ async function workspaceAcceptCommand(tokenOrUrl) {
3606
+ const token = inviteToken(tokenOrUrl);
3607
+ const resp = await api(`/invites/${encodeURIComponent(token)}/accept`, {
3608
+ method: "POST",
3609
+ authRequired: true
3610
+ });
3611
+ log.ok(`Workspace invite ${resp.status}${resp.role ? ` as ${resp.role}` : ""}`);
3612
+ }
3499
3613
 
3500
3614
  // src/lib/config-file.ts
3501
3615
  import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
@@ -3912,18 +4026,33 @@ async function statusCommand(opts = {}) {
3912
4026
  const scope = normalizeScope(opts.scope);
3913
4027
  const config = await readFloomConfig(scope);
3914
4028
  const active = config.targets[target]?.active_workspaces ?? [];
3915
- log.heading(`Floom status for ${target} (${scope})`);
3916
- log.kv("Default workspace", config.default_workspace ?? "(none)");
3917
- log.kv("Active workspaces", active.length ? active.join(", ") : "(none)");
3918
- log.kv("Local pull policy", "router + pinned skills + instructions only");
4029
+ const workspaces = [];
3919
4030
  for (const workspace of active) {
3920
4031
  const pins = await api(`/libraries/${workspace}/pins`, {
3921
4032
  authRequired: true,
3922
4033
  query: { target }
3923
4034
  });
4035
+ workspaces.push({ workspace, pins: pins.pins });
4036
+ }
4037
+ if (opts.json) {
4038
+ console.log(JSON.stringify({
4039
+ target,
4040
+ scope,
4041
+ default_workspace: config.default_workspace ?? null,
4042
+ active_workspaces: active,
4043
+ pull_policy: "router + pinned skills + instructions only",
4044
+ workspaces
4045
+ }, null, 2));
4046
+ return;
4047
+ }
4048
+ log.heading(`Floom status for ${target} (${scope})`);
4049
+ log.kv("Default workspace", config.default_workspace ?? "(none)");
4050
+ log.kv("Active workspaces", active.length ? active.join(", ") : "(none)");
4051
+ log.kv("Local pull policy", "router + pinned skills + instructions only");
4052
+ for (const workspace of workspaces) {
3924
4053
  log.blank();
3925
- log.info(`${workspace}: ${pins.pins.length} pinned skill(s) for ${target}`);
3926
- for (const pin of pins.pins) {
4054
+ log.info(`${workspace.workspace}: ${workspace.pins.length} pinned skill(s) for ${target}`);
4055
+ for (const pin of workspace.pins) {
3927
4056
  log.kv("", `${pin.skill?.slug ?? pin.skill_id}${pin.skill?.latest?.version ? `@${pin.skill.latest.version}` : ""}`);
3928
4057
  }
3929
4058
  }
@@ -3984,6 +4113,29 @@ async function pinCommand(ref, opts = {}) {
3984
4113
  });
3985
4114
  log.ok(`Pinned ${ref} for ${target} in ${workspace}`);
3986
4115
  }
4116
+ async function pinnedCommand(opts = {}) {
4117
+ const target = assertTarget(opts.target ?? "codex");
4118
+ const workspace = await resolveWorkspace(opts);
4119
+ const resp = await api(`/libraries/${workspace}/pins`, {
4120
+ authRequired: true,
4121
+ query: { target }
4122
+ });
4123
+ if (opts.json) {
4124
+ console.log(JSON.stringify(resp, null, 2));
4125
+ return;
4126
+ }
4127
+ if (resp.pins.length === 0) {
4128
+ log.info(`No pinned skills for ${target} in ${workspace}.`);
4129
+ return;
4130
+ }
4131
+ log.heading(`Pinned skills for ${target} in ${workspace}`);
4132
+ for (const pin of resp.pins) {
4133
+ const ref = pin.skill?.slug ? `${workspace}/${pin.skill.slug}` : pin.skill_id;
4134
+ const version = pin.skill?.latest?.version ? `@${pin.skill.latest.version}` : "";
4135
+ const title = pin.skill?.title ? ` ${pin.skill.title}` : "";
4136
+ console.log(` ${`${ref}${version}`.padEnd(42)} ${pin.created_at}${title}`);
4137
+ }
4138
+ }
3987
4139
  async function unpinCommand(ref, opts = {}) {
3988
4140
  const target = assertTarget(opts.target ?? "codex");
3989
4141
  const workspace = await resolveWorkspace(opts);
@@ -3995,6 +4147,78 @@ async function unpinCommand(ref, opts = {}) {
3995
4147
  log.ok(`Unpinned ${ref} for ${target} in ${workspace}`);
3996
4148
  }
3997
4149
 
4150
+ // src/commands/folder.ts
4151
+ function slugify(value) {
4152
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
4153
+ }
4154
+ async function folderListCommand(workspace) {
4155
+ const resp = await api(`/libraries/${encodeURIComponent(workspace)}/folders`, { authRequired: true });
4156
+ if (resp.folders.length === 0) {
4157
+ log.info("No folders.");
4158
+ return;
4159
+ }
4160
+ for (const folder of resp.folders) {
4161
+ console.log(`${folder.id} ${folder.slug.padEnd(24)} ${folder.access_mode.padEnd(10)} ${String(folder.skills_count ?? 0).padStart(3)} skills ${folder.name}`);
4162
+ }
4163
+ }
4164
+ async function folderCreateCommand(workspace, name, opts = {}) {
4165
+ const slug = opts.slug ?? slugify(name);
4166
+ const folder = await api(`/libraries/${encodeURIComponent(workspace)}/folders`, {
4167
+ method: "POST",
4168
+ authRequired: true,
4169
+ body: {
4170
+ name,
4171
+ slug,
4172
+ access_mode: opts.access ?? "open",
4173
+ public_release_policy: opts.publicRelease ?? "allowed",
4174
+ default_visibility: opts.visibility ?? "private"
4175
+ }
4176
+ });
4177
+ log.ok(`Created folder ${folder.slug} in ${workspace}.`);
4178
+ log.kv("Folder ID", folder.id);
4179
+ }
4180
+ async function folderArchiveCommand(workspace, folderId) {
4181
+ await api(`/libraries/${encodeURIComponent(workspace)}/folders/${encodeURIComponent(folderId)}`, {
4182
+ method: "DELETE",
4183
+ authRequired: true
4184
+ });
4185
+ log.ok(`Archived folder ${folderId}.`);
4186
+ }
4187
+ async function folderMoveCommand(ref, folderId, opts = {}) {
4188
+ const cleaned = ref.replace(/^@/, "");
4189
+ const parts = cleaned.split("/");
4190
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
4191
+ throw new Error("Expected ref format: workspace/skill or @workspace/skill");
4192
+ }
4193
+ const [owner, slug] = parts;
4194
+ const targetFolderId = opts.root ? null : folderId;
4195
+ if (!opts.root && !targetFolderId) throw new Error("Provide a folder id, or pass --root to move to workspace root.");
4196
+ const resp = await api(`/skills/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}/move`, {
4197
+ method: "POST",
4198
+ authRequired: true,
4199
+ body: {
4200
+ target_library_slug: opts.workspace,
4201
+ target_folder_id: targetFolderId
4202
+ }
4203
+ });
4204
+ log.ok(`Moved ${ref} to ${resp.folder_id ? `folder ${resp.folder_id}` : "workspace root"} in ${resp.library_slug}.`);
4205
+ }
4206
+ async function folderGrantCommand(workspace, folderId, email, role) {
4207
+ await api(`/libraries/${encodeURIComponent(workspace)}/folders/${encodeURIComponent(folderId)}/members`, {
4208
+ method: "POST",
4209
+ authRequired: true,
4210
+ body: { email, role }
4211
+ });
4212
+ log.ok(`Granted ${role} folder access to ${email}.`);
4213
+ }
4214
+ async function folderRevokeCommand(workspace, folderId, memberId) {
4215
+ await api(`/libraries/${encodeURIComponent(workspace)}/folders/${encodeURIComponent(folderId)}/members/${encodeURIComponent(memberId)}`, {
4216
+ method: "DELETE",
4217
+ authRequired: true
4218
+ });
4219
+ log.ok(`Revoked folder member ${memberId}.`);
4220
+ }
4221
+
3998
4222
  // src/commands/mcp.ts
3999
4223
  import { mkdtemp, mkdir as mkdir9, readdir as readdir4, readFile as readFile10, rename as rename3, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
4000
4224
  import { join as join13 } from "node:path";
@@ -4016,7 +4240,7 @@ async function fetchWithTimeout2(url, init = {}) {
4016
4240
  const controller = new AbortController();
4017
4241
  const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
4018
4242
  try {
4019
- return await fetch(url, { ...init, signal: controller.signal });
4243
+ return await fetch(url, { ...init, redirect: "manual", signal: controller.signal });
4020
4244
  } catch (e) {
4021
4245
  if (e.name === "AbortError") throw new Error(`Request timed out after ${API_TIMEOUT_MS}ms`);
4022
4246
  throw e;
@@ -4056,6 +4280,12 @@ async function apiRequest(token, path, query) {
4056
4280
  lastError = new Error(`Unable to reach Floom API at ${base}: ${e.message}`);
4057
4281
  continue;
4058
4282
  }
4283
+ if (res.status >= 300 && res.status < 400) {
4284
+ const location = res.headers.get("location");
4285
+ const redirectHost2 = location ? new URL(location, url).host : "unknown";
4286
+ lastError = new Error(`Floom API returned an unexpected redirect to ${redirectHost2}. Refusing to forward credentials.`);
4287
+ break;
4288
+ }
4059
4289
  const body = await res.text();
4060
4290
  let json = null;
4061
4291
  try {
@@ -4397,11 +4627,12 @@ program.command("install <ref>").description("Install a skill (default: .agents/
4397
4627
  program.command("installed").description("List installed skills in this project.").option("--json").action(installedCommand);
4398
4628
  program.command("outdated").description("Show installed skills with newer versions available.").action(outdatedCommand);
4399
4629
  program.command("update [ref]").description("Update installed skills to latest.").option("--force", "Overwrite local edits").action((ref, opts) => updateCommand(ref, opts));
4400
- 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);
4630
+ 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);
4401
4631
  program.command("info <ref>").description("Show details for a remote skill.").action(infoCommand);
4402
- 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));
4632
+ 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));
4403
4633
  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));
4404
4634
  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));
4635
+ 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));
4405
4636
  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));
4406
4637
  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));
4407
4638
  program.command("unshare <ref> <email>").description("Revoke someone's access.").action((ref, email) => unshareCommand(ref, email));
@@ -4411,6 +4642,8 @@ function addWorkspaceCommands(cmd) {
4411
4642
  cmd.command("list").action(libraryListCommand);
4412
4643
  cmd.command("create <slug> <name>").action((slug, name) => libraryCreateCommand(slug, name));
4413
4644
  cmd.command("invite <workspaceSlug> <email>").option("--role <role>", "viewer|editor|admin", "viewer").action((workspaceSlug, email, opts) => libraryInviteCommand(workspaceSlug, email, opts.role));
4645
+ cmd.command("invites").description("List pending workspace invites for the logged-in user").action(workspaceInvitesCommand);
4646
+ cmd.command("accept <tokenOrUrl>").description("Accept a workspace invite token or URL").action((tokenOrUrl) => workspaceAcceptCommand(tokenOrUrl));
4414
4647
  cmd.command("leave <workspaceSlug>").action((workspaceSlug) => libraryLeaveCommand(workspaceSlug));
4415
4648
  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));
4416
4649
  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));
@@ -4418,6 +4651,13 @@ function addWorkspaceCommands(cmd) {
4418
4651
  }
4419
4652
  addWorkspaceCommands(program.command("workspace").description("Manage workspaces"));
4420
4653
  addWorkspaceCommands(program.command("library").description("Manage workspaces (legacy alias)"));
4654
+ var folderCmd = program.command("folder").description("Manage workspace folders");
4655
+ folderCmd.command("list <workspace>").description("List folders in a workspace").action((workspace) => folderListCommand(workspace));
4656
+ 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));
4657
+ folderCmd.command("archive <workspace> <folderId>").description("Archive an empty folder").action((workspace, folderId) => folderArchiveCommand(workspace, folderId));
4658
+ 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));
4659
+ 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));
4660
+ folderCmd.command("revoke <workspace> <folderId> <memberId>").description("Revoke a folder member grant").action((workspace, folderId, memberId) => folderRevokeCommand(workspace, folderId, memberId));
4421
4661
  var instructionCmd = program.command("instruction").description("Manage account and workspace instructions");
4422
4662
  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));
4423
4663
  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));
@@ -4434,8 +4674,13 @@ async function main() {
4434
4674
  }
4435
4675
  process.exit(1);
4436
4676
  }
4437
- log.err(e.message ?? "Unknown error");
4438
- if (process.env.FLOOM_DEBUG) console.error(e);
4677
+ const error = e;
4678
+ log.err(error.message ?? "Unknown error");
4679
+ if (process.env.FLOOM_DEBUG) {
4680
+ const name = error.name || "Error";
4681
+ const code = error.code ? ` code=${error.code}` : "";
4682
+ console.error(`[debug] ${name}${code}`);
4683
+ }
4439
4684
  process.exit(1);
4440
4685
  }
4441
4686
  }