@floomhq/floom 2.0.2 → 2.0.4

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
@@ -2477,6 +2477,32 @@ var ContractDecisionChecklistItemSchema = z2.object({
2477
2477
  title: z2.string().min(1),
2478
2478
  locked: z2.literal(true)
2479
2479
  }).strict();
2480
+ var AGENT_TARGETS = ["claude", "codex", "cursor", "gemini", "opencode"];
2481
+ var AgentTargetSyncSchema = z2.object({
2482
+ last_pulled_at: IsoDateTimeSchema.nullable()
2483
+ }).strict();
2484
+ var AgentMachineSchema = z2.object({
2485
+ machine_id: z2.string().uuid(),
2486
+ machine_label: z2.string().min(1),
2487
+ last_seen_at: IsoDateTimeSchema,
2488
+ target_sync: z2.record(z2.enum(AGENT_TARGETS), AgentTargetSyncSchema)
2489
+ }).strict();
2490
+ var AgentsListResponseSchema = z2.object({
2491
+ machines: z2.array(AgentMachineSchema),
2492
+ total: z2.number().int().nonnegative()
2493
+ }).strict();
2494
+ var ActivityEventSchema = z2.object({
2495
+ id: IdSchema,
2496
+ event_name: z2.enum(["signup", "workspace_create", "push", "pull", "share_create", "skill_delete"]),
2497
+ source: z2.enum(["web", "cli", "api"]),
2498
+ target: z2.enum(AGENT_TARGETS).nullable(),
2499
+ skill_slug: z2.string().nullable(),
2500
+ created_at: IsoDateTimeSchema
2501
+ }).strict();
2502
+ var ActivityFeedResponseSchema = z2.object({
2503
+ events: z2.array(ActivityEventSchema),
2504
+ total: z2.number().int().nonnegative()
2505
+ }).strict();
2480
2506
 
2481
2507
  // src/lib/output.ts
2482
2508
  import chalk from "chalk";
@@ -2563,14 +2589,20 @@ async function readRawAuth() {
2563
2589
  const code = e.code;
2564
2590
  if (code === "ENOENT") return null;
2565
2591
  if (code === "EACCES" || code === "EPERM") {
2566
- throw new Error("Cannot read your Floom auth file (permission denied). Fix its permissions or run: floom login");
2592
+ throw new Error(
2593
+ "Cannot read your Floom auth file (permission denied).\n Fix: chmod 600 ~/.floom/auth.json\n Or re-authenticate: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2594
+ );
2567
2595
  }
2568
- throw new Error("Cannot read your Floom auth file. Run: floom login");
2596
+ throw new Error(
2597
+ "Cannot read your Floom auth file.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2598
+ );
2569
2599
  }
2570
2600
  try {
2571
2601
  return JSON.parse(raw);
2572
2602
  } catch {
2573
- throw new Error("Your Floom auth file is unreadable. Run: floom login");
2603
+ throw new Error(
2604
+ "Your Floom auth file is corrupted.\n Run: npx -y @floomhq/floom login to generate a fresh one.\n More help: https://floom.dev/docs#troubleshooting"
2605
+ );
2574
2606
  }
2575
2607
  }
2576
2608
  async function writeAuth(state) {
@@ -2692,7 +2724,7 @@ async function getMachineIdentity() {
2692
2724
  }
2693
2725
 
2694
2726
  // src/version.ts
2695
- var VERSION = "2.0.2";
2727
+ var VERSION = "2.0.4";
2696
2728
 
2697
2729
  // src/api-client.ts
2698
2730
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -2739,11 +2771,54 @@ async function readLimitedText(res, limitBytes = MAX_ERROR_BODY_BYTES) {
2739
2771
  return truncated ? `${text}
2740
2772
  [truncated]` : text;
2741
2773
  }
2774
+ var HELP = "https://floom.dev/docs#troubleshooting";
2775
+ function friendlyApiErrorMessage(code, raw, status) {
2776
+ if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
2777
+ return `Not signed in.
2778
+ Run: npx -y @floomhq/floom login
2779
+ More help: ${HELP}`;
2780
+ }
2781
+ if (code === "TOKEN_EXPIRED") {
2782
+ return `Your session has expired.
2783
+ Run: npx -y @floomhq/floom login
2784
+ More help: ${HELP}`;
2785
+ }
2786
+ if (code === "SKILL_ACCESS_DENIED" || status === 403) {
2787
+ return `Access denied. This skill belongs to another workspace or you don't have permission to modify it.
2788
+ If you want your own copy, push it with a different slug. More help: ${HELP}`;
2789
+ }
2790
+ if (code === "SKILL_NOT_FOUND" || status === 404) {
2791
+ return `Skill not found. It may have been deleted or the slug is wrong.
2792
+ Run: npx -y @floomhq/floom list
2793
+ More help: ${HELP}`;
2794
+ }
2795
+ if (code === "RATE_LIMITED" || status === 429) {
2796
+ return `Too many requests. Wait a moment and try again.
2797
+ More help: ${HELP}`;
2798
+ }
2799
+ if (code === "CLI_VERSION_TOO_OLD") {
2800
+ return `Your CLI is out of date.
2801
+ Run: npx -y @floomhq/floom@latest <command>
2802
+ More help: ${HELP}`;
2803
+ }
2804
+ if (code === "FILE_TOO_LARGE" || code === "BUNDLE_TOO_LARGE") {
2805
+ return `${raw}
2806
+ Reduce the size of your skill files before pushing. More help: ${HELP}`;
2807
+ }
2808
+ if (status && status >= 500) {
2809
+ return `Floom service error (${status}). Please try again in a moment.
2810
+ More help: ${HELP}`;
2811
+ }
2812
+ return raw;
2813
+ }
2742
2814
  async function api(path, opts = {}) {
2743
2815
  const auth = await readAuth();
2744
2816
  const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
2745
2817
  if (opts.authRequired && !token) {
2746
- throw new FloomError("AUTH_REQUIRED", "Not logged in. Run: npx -y @floomhq/floom login");
2818
+ throw new FloomError(
2819
+ "AUTH_REQUIRED",
2820
+ "Not signed in.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2821
+ );
2747
2822
  }
2748
2823
  let lastError = null;
2749
2824
  const bases = getApiBaseUrls(auth?.apiUrl);
@@ -2776,7 +2851,9 @@ async function api(path, opts = {}) {
2776
2851
  } catch (e) {
2777
2852
  lastError = new FloomError(
2778
2853
  "INTERNAL_ERROR",
2779
- `Unable to reach Floom API at ${base}: ${e.message}`,
2854
+ `Cannot reach Floom (${base}): ${e.message}
2855
+ Check your internet connection or firewall. If on a corporate network, you may need to set a proxy: npm config set proxy http://...
2856
+ More help: https://floom.dev/docs#troubleshooting`,
2780
2857
  { apiUrl: base }
2781
2858
  );
2782
2859
  continue;
@@ -2796,17 +2873,19 @@ async function api(path, opts = {}) {
2796
2873
  }
2797
2874
  const envelopeError = json?.error;
2798
2875
  if (envelopeError && typeof envelopeError === "object" && envelopeError.code) {
2876
+ const rawMsg2 = typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code);
2799
2877
  throw new FloomError(
2800
2878
  envelopeError.code,
2801
- typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code),
2879
+ friendlyApiErrorMessage(envelopeError.code, rawMsg2, res.status),
2802
2880
  { status: res.status, requestId: envelopeError.request_id, apiUrl: base }
2803
2881
  );
2804
2882
  }
2805
2883
  if (res.ok) return json;
2806
2884
  const err = envelopeError ?? {};
2885
+ const rawMsg = err.message ?? `HTTP ${res.status} ${res.statusText}`;
2807
2886
  lastError = new FloomError(
2808
2887
  err.code ?? "INTERNAL_ERROR",
2809
- err.message ?? `HTTP ${res.status} ${res.statusText}`,
2888
+ friendlyApiErrorMessage(err.code ?? "INTERNAL_ERROR", rawMsg, res.status),
2810
2889
  { status: res.status, requestId: err.request_id, apiUrl: base }
2811
2890
  );
2812
2891
  if (res.status !== 404 || json?.error) break;
@@ -2862,7 +2941,9 @@ async function loginCommand() {
2862
2941
  if (remaining <= 0) break;
2863
2942
  await sleep(Math.min(interval, remaining));
2864
2943
  }
2865
- throw new Error("Login timed out.");
2944
+ throw new Error(
2945
+ "Login timed out.\n The browser approval window expired. Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2946
+ );
2866
2947
  }
2867
2948
 
2868
2949
  // src/commands/mcp.ts
@@ -2936,7 +3017,9 @@ async function readManifest(root) {
2936
3017
  if (err.code === "ENOENT") return null;
2937
3018
  if (error instanceof SyntaxError || error instanceof ZodError) {
2938
3019
  throw new Error(
2939
- `Corrupted manifest at ${manifestPath(root)}. Remove that file or run: npx -y @floomhq/floom pull`
3020
+ `Corrupted manifest at ${manifestPath(root)}. Remove that file or run floom pull after backing up local edits.
3021
+ Run: rm ${manifestPath(root)} && npx -y @floomhq/floom pull
3022
+ More help: https://floom.dev/docs#troubleshooting`
2940
3023
  );
2941
3024
  }
2942
3025
  throw error;
@@ -3148,8 +3231,11 @@ function printDetectedTargets(targets) {
3148
3231
  for (const target of targets) log.info(`- ${target}`);
3149
3232
  }
3150
3233
  function printNoDetectedTargets() {
3151
- log.warn(`No installed agents detected. Checked: ${INSTALL_TARGETS.join(", ")}.`);
3152
- log.info(`Use --target <${INSTALL_TARGETS.join("|")}> to choose one target explicitly.`);
3234
+ log.err(`No AI agent recognized on this machine.`);
3235
+ log.info(` Floom looks for: ${INSTALL_TARGETS.join(", ")}.`);
3236
+ log.info(` Make sure one of these is installed, then re-run.`);
3237
+ log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
3238
+ log.info(` More help: https://floom.dev/docs#agents`);
3153
3239
  }
3154
3240
  async function statusCommand(options) {
3155
3241
  if (!options.target) {
@@ -3563,11 +3649,17 @@ async function findImmediateSkillDirs(root) {
3563
3649
  async function pushOneSkill(root, pushApi) {
3564
3650
  const dirStat = await stat4(root).catch(() => null);
3565
3651
  if (!dirStat || !dirStat.isDirectory()) {
3566
- throw new Error(`Directory not found: ${root}`);
3652
+ throw new Error(
3653
+ `Folder not found: ${root}
3654
+ Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
3655
+ );
3567
3656
  }
3568
3657
  const bundle = await collectBundle(root);
3569
3658
  const skillMd = bundle.files.find((file) => file.relPath === "SKILL.md");
3570
- if (!skillMd) throw new Error("SKILL.md is required.");
3659
+ if (!skillMd) throw new Error(
3660
+ `Missing SKILL.md in ${root}.
3661
+ Every skill folder needs a SKILL.md at its root. See: https://floom.dev/docs#concept`
3662
+ );
3571
3663
  const files = await Promise.all(bundle.files.map(async (file) => ({
3572
3664
  path: file.relPath,
3573
3665
  content_base64: (await readFile6(file.absPath)).toString("base64")
@@ -3598,7 +3690,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3598
3690
  const root = resolve2(dir);
3599
3691
  const dirStat = await stat4(root).catch(() => null);
3600
3692
  if (!dirStat || !dirStat.isDirectory()) {
3601
- throw new Error(`Directory not found: ${dir}`);
3693
+ throw new Error(
3694
+ `Folder not found: ${dir}
3695
+ Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
3696
+ );
3602
3697
  }
3603
3698
  const pushApi = deps.pushApi ?? api;
3604
3699
  if (await hasSkillMarkdown(root)) {
@@ -3607,7 +3702,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3607
3702
  return;
3608
3703
  }
3609
3704
  const skillDirs = await findImmediateSkillDirs(root);
3610
- if (skillDirs.length === 0) throw new Error("SKILL.md is required.");
3705
+ if (skillDirs.length === 0) throw new Error(
3706
+ `No skills found in ${dir}.
3707
+ Neither the folder itself nor any immediate subfolder contains a SKILL.md. See: https://floom.dev/docs#concept`
3708
+ );
3611
3709
  const concurrency = parseConcurrency(options.concurrency);
3612
3710
  const startedAt = Date.now();
3613
3711
  const errors = [];
@@ -3704,6 +3802,219 @@ async function renameMachineCommand(newLabel, _opts) {
3704
3802
  }
3705
3803
  }
3706
3804
 
3805
+ // src/commands/add.ts
3806
+ import { mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
3807
+ import { join as join8 } from "node:path";
3808
+ var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
3809
+ function parseToken(input) {
3810
+ const trimmed = input.trim();
3811
+ if (TOKEN_RE.test(trimmed)) return trimmed;
3812
+ try {
3813
+ const url = new URL(trimmed);
3814
+ const parts = url.pathname.split("/").filter(Boolean);
3815
+ if (parts.length >= 2 && parts[parts.length - 2] === "s") {
3816
+ const candidate = parts[parts.length - 1];
3817
+ if (candidate && TOKEN_RE.test(candidate)) return candidate;
3818
+ }
3819
+ const last = parts[parts.length - 1];
3820
+ if (last && TOKEN_RE.test(last)) return last;
3821
+ } catch {
3822
+ }
3823
+ return null;
3824
+ }
3825
+ var SAFE_SLUG_RE2 = /^(?=.{1,80}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
3826
+ var MAX_DECODED_FILE_BYTES2 = 2 * 1024 * 1024;
3827
+ function safeSkillSlug2(slug) {
3828
+ if (!slug) throw new Error("Remote skill slug is empty.");
3829
+ if (slug.length > 80) throw new Error(`Remote skill slug "${slug.slice(0, 80)}\u2026" exceeds 80 characters.`);
3830
+ if (!SAFE_SLUG_RE2.test(slug)) throw new Error(`Unsafe remote skill slug: ${slug}`);
3831
+ return slug;
3832
+ }
3833
+ function safeRemotePath2(path) {
3834
+ const normalized = path.replace(/\\/g, "/");
3835
+ if (!normalized || /[\x00-\x1f\x7f]/.test(normalized) || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
3836
+ throw new Error(`Unsafe remote file path: ${path}`);
3837
+ }
3838
+ return normalized;
3839
+ }
3840
+ function bytesForShareFile(file) {
3841
+ if (file.mode === "text") {
3842
+ const text = file.content_text ?? "";
3843
+ if (Buffer.byteLength(text, "utf8") > MAX_DECODED_FILE_BYTES2) {
3844
+ throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
3845
+ }
3846
+ return Buffer.from(text, "utf8");
3847
+ }
3848
+ const url = file.binary?.download_url ?? null;
3849
+ if (!url?.startsWith("data:")) return Buffer.alloc(0);
3850
+ const [, encoded] = url.split(",", 2);
3851
+ const base64 = encoded ?? "";
3852
+ const bytes = Buffer.from(base64, "base64");
3853
+ if (bytes.byteLength > MAX_DECODED_FILE_BYTES2) {
3854
+ throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
3855
+ }
3856
+ return bytes;
3857
+ }
3858
+ function derivedSlug(title) {
3859
+ return title.toLowerCase().replace(/[^a-z0-9 -]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 80).replace(/^-+|-+$/g, "") || "shared-skill";
3860
+ }
3861
+ async function fetchPublicShare(token) {
3862
+ const apiBase = getAppUrl().replace(/\/$/, "");
3863
+ const url = `${apiBase}/api/v1/share/${encodeURIComponent(token)}`;
3864
+ const controller = new AbortController();
3865
+ const timer = setTimeout(() => controller.abort(), 3e4);
3866
+ let res;
3867
+ try {
3868
+ res = await fetch(url, {
3869
+ headers: { "User-Agent": "floom-cli/add" },
3870
+ redirect: "manual",
3871
+ signal: controller.signal
3872
+ });
3873
+ } catch (e) {
3874
+ if (e.name === "AbortError") {
3875
+ throw new Error("Request timed out. Check your internet connection and try again.");
3876
+ }
3877
+ throw new Error(`Cannot reach Floom: ${e.message}`);
3878
+ } finally {
3879
+ clearTimeout(timer);
3880
+ }
3881
+ if (res.status === 404) {
3882
+ throw new FloomError(
3883
+ "SKILL_NOT_FOUND",
3884
+ "Share link not found or revoked. Ask the sender for a fresh link."
3885
+ );
3886
+ }
3887
+ if (res.status === 429) {
3888
+ throw new FloomError(
3889
+ "RATE_LIMITED",
3890
+ "Too many requests. Try again in a minute."
3891
+ );
3892
+ }
3893
+ if (!res.ok) {
3894
+ throw new Error(`Floom API returned HTTP ${res.status}. Try again in a moment.`);
3895
+ }
3896
+ let json;
3897
+ try {
3898
+ json = await res.json();
3899
+ } catch {
3900
+ throw new Error("Unexpected response from Floom API. Try again in a moment.");
3901
+ }
3902
+ if (json.error) {
3903
+ throw new Error(json.error.message ?? "API error");
3904
+ }
3905
+ if (!json.data) {
3906
+ throw new Error("Unexpected response shape from Floom API.");
3907
+ }
3908
+ return json.data;
3909
+ }
3910
+ async function addCommand(input, opts = {}) {
3911
+ const token = parseToken(input);
3912
+ if (!token) {
3913
+ log.err(
3914
+ "That doesn't look like a Floom share link. Should look like: https://floom.dev/s/fls_xxx"
3915
+ );
3916
+ process.exitCode = 1;
3917
+ return;
3918
+ }
3919
+ let target;
3920
+ if (opts.target) {
3921
+ try {
3922
+ target = assertInstallTarget(opts.target);
3923
+ } catch {
3924
+ log.err(`Unknown target "${opts.target}". Use one of: claude, codex, cursor, gemini, opencode`);
3925
+ process.exitCode = 1;
3926
+ return;
3927
+ }
3928
+ } else {
3929
+ const detected = await detectInstalledTargets();
3930
+ if (detected.length === 0) {
3931
+ log.err("No AI agent recognized on this machine.");
3932
+ log.info(" Floom looks for: claude, codex, cursor, gemini, opencode.");
3933
+ log.info(" Or pick one explicitly: npx -y @floomhq/floom add <share-url> --target claude");
3934
+ process.exitCode = 1;
3935
+ return;
3936
+ }
3937
+ target = detected[0];
3938
+ }
3939
+ const resolved = resolveInstallDir({ target, global: true });
3940
+ log.step(`Fetching share\u2026`);
3941
+ let shareData;
3942
+ try {
3943
+ shareData = await fetchPublicShare(token);
3944
+ } catch (e) {
3945
+ if (e instanceof FloomError) {
3946
+ log.err(e.message);
3947
+ } else {
3948
+ log.err(e.message ?? "Unknown error");
3949
+ }
3950
+ process.exitCode = 1;
3951
+ return;
3952
+ }
3953
+ const slug = safeSkillSlug2(derivedSlug(shareData.skill.title));
3954
+ for (const file of shareData.file_contents) {
3955
+ safeRemotePath2(file.path);
3956
+ }
3957
+ const cleanup = installCancellationHandler();
3958
+ const tempDir = join8(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
3959
+ cleanup.trackDir(tempDir);
3960
+ try {
3961
+ await mkdir5(tempDir, { recursive: true });
3962
+ for (const file of shareData.file_contents) {
3963
+ const safePath = safeRemotePath2(file.path);
3964
+ const dest = join8(tempDir, ...safePath.split("/"));
3965
+ const destDir = dest.substring(0, dest.lastIndexOf("/"));
3966
+ if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
3967
+ await writeFile5(dest, bytesForShareFile(file));
3968
+ }
3969
+ const finalDir = join8(resolved.dir, slug);
3970
+ const replacedDir = join8(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
3971
+ let movedExisting = false;
3972
+ try {
3973
+ const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
3974
+ await mkdir5(resolved.dir, { recursive: true });
3975
+ const { stat: stat5 } = await import("node:fs/promises");
3976
+ let existingDir = false;
3977
+ try {
3978
+ await stat5(finalDir);
3979
+ existingDir = true;
3980
+ } catch {
3981
+ }
3982
+ if (existingDir) {
3983
+ await rename2(finalDir, replacedDir);
3984
+ movedExisting = true;
3985
+ }
3986
+ try {
3987
+ await rename2(tempDir, finalDir);
3988
+ } catch (renameErr) {
3989
+ if (movedExisting) {
3990
+ try {
3991
+ await rename2(replacedDir, finalDir);
3992
+ } catch {
3993
+ }
3994
+ }
3995
+ throw new Error(`Failed to install ${slug}: ${renameErr.message}`);
3996
+ }
3997
+ if (movedExisting) await rmFs(replacedDir, { recursive: true, force: true });
3998
+ } catch (e) {
3999
+ throw e;
4000
+ }
4001
+ log.blank();
4002
+ log.ok(`Installed ${shareData.skill.title} ${shareData.skill.version.display} from ${shareData.skill.owner.name}`);
4003
+ log.info(` \u2192 ${finalDir}`);
4004
+ log.blank();
4005
+ log.info(`Next: in your agent, the skill is now loaded under the slug "${slug}".`);
4006
+ } catch (e) {
4007
+ log.err(e.message ?? "Unknown error");
4008
+ process.exitCode = 1;
4009
+ } finally {
4010
+ cleanup.dispose();
4011
+ try {
4012
+ await rm2(tempDir, { recursive: true, force: true });
4013
+ } catch {
4014
+ }
4015
+ }
4016
+ }
4017
+
3707
4018
  // src/index.ts
3708
4019
  async function logoutCommand() {
3709
4020
  const auth = await readAuth();
@@ -3720,7 +4031,9 @@ async function logoutCommand() {
3720
4031
  async function whoamiCommand() {
3721
4032
  const auth = await readAuth();
3722
4033
  if (!auth) {
3723
- log.info("Not logged in. Run: npx -y @floomhq/floom login");
4034
+ log.err("Not signed in.");
4035
+ log.info(" Run: npx -y @floomhq/floom login");
4036
+ log.info(" More help: https://floom.dev/docs#troubleshooting");
3724
4037
  process.exitCode = 1;
3725
4038
  return;
3726
4039
  }
@@ -3739,12 +4052,9 @@ async function whoamiCommand() {
3739
4052
  }
3740
4053
  log.kv("api url", auth.apiUrl);
3741
4054
  } catch (error) {
3742
- if (error instanceof FloomError) {
3743
- log.err(`${error.code}: ${error.message}`);
3744
- } else {
3745
- log.err(error.message);
3746
- }
3747
- log.warn("Local auth file exists but the server rejected this session. Run: npx -y @floomhq/floom login");
4055
+ log.err(error.message);
4056
+ log.warn("Session invalid. Run: npx -y @floomhq/floom login");
4057
+ log.info(" More help: https://floom.dev/docs#troubleshooting");
3748
4058
  process.exitCode = 1;
3749
4059
  }
3750
4060
  }
@@ -3759,13 +4069,14 @@ program.command("pull").description("Pull the whole workspace library.").option(
3759
4069
  program.command("list").description("List workspace skills.").action(listCommand);
3760
4070
  program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
3761
4071
  program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
4072
+ program.command("add <share-url-or-token>").description("Install a skill from a Floom share link.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action((input, opts) => addCommand(input, opts));
3762
4073
  program.command("rename-machine <label>").description('Set the friendly name for THIS machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
3763
4074
  async function main() {
3764
4075
  try {
3765
4076
  await program.parseAsync(process.argv);
3766
4077
  } catch (e) {
3767
4078
  if (e instanceof FloomError) {
3768
- log.err(`${e.code}: ${e.message}`);
4079
+ log.err(e.message);
3769
4080
  process.exit(1);
3770
4081
  }
3771
4082
  log.err(e.message ?? "Unknown error");
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "2.0.2";
1
+ export const VERSION = "2.0.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Floom CLI \u2014 one shared skill library, pulled into the AI agent you choose (Claude, Codex, Cursor, Gemini, OpenCode).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://floom.dev",