@floomhq/floom 2.0.3 → 2.0.5

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,46 @@ 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();
2506
+ var SkillSyncTargetEntrySchema = z2.object({
2507
+ target: TargetSchema,
2508
+ last_pulled_at: IsoDateTimeSchema
2509
+ }).strict();
2510
+ var SkillSyncMachineSchema = z2.object({
2511
+ machine_id: IdSchema,
2512
+ machine_label: z2.string().min(1),
2513
+ targets: z2.array(SkillSyncTargetEntrySchema)
2514
+ }).strict();
2515
+ var SkillSyncStateResponseSchema = z2.object({
2516
+ machines: z2.array(SkillSyncMachineSchema),
2517
+ total_machines: z2.number().int().nonnegative(),
2518
+ total_targets_synced: z2.number().int().nonnegative()
2519
+ }).strict();
2480
2520
 
2481
2521
  // src/lib/output.ts
2482
2522
  import chalk from "chalk";
@@ -2563,14 +2603,20 @@ async function readRawAuth() {
2563
2603
  const code = e.code;
2564
2604
  if (code === "ENOENT") return null;
2565
2605
  if (code === "EACCES" || code === "EPERM") {
2566
- throw new Error("Cannot read your Floom auth file (permission denied). Fix its permissions or run: floom login");
2606
+ throw new Error(
2607
+ "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"
2608
+ );
2567
2609
  }
2568
- throw new Error("Cannot read your Floom auth file. Run: floom login");
2610
+ throw new Error(
2611
+ "Cannot read your Floom auth file.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2612
+ );
2569
2613
  }
2570
2614
  try {
2571
2615
  return JSON.parse(raw);
2572
2616
  } catch {
2573
- throw new Error("Your Floom auth file is unreadable. Run: floom login");
2617
+ throw new Error(
2618
+ "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"
2619
+ );
2574
2620
  }
2575
2621
  }
2576
2622
  async function writeAuth(state) {
@@ -2692,7 +2738,7 @@ async function getMachineIdentity() {
2692
2738
  }
2693
2739
 
2694
2740
  // src/version.ts
2695
- var VERSION = "2.0.3";
2741
+ var VERSION = "2.0.4";
2696
2742
 
2697
2743
  // src/api-client.ts
2698
2744
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -2739,11 +2785,54 @@ async function readLimitedText(res, limitBytes = MAX_ERROR_BODY_BYTES) {
2739
2785
  return truncated ? `${text}
2740
2786
  [truncated]` : text;
2741
2787
  }
2788
+ var HELP = "https://floom.dev/docs#troubleshooting";
2789
+ function friendlyApiErrorMessage(code, raw, status) {
2790
+ if (code === "AUTH_REQUIRED" || code === "TOKEN_INVALID" || status === 401) {
2791
+ return `Not signed in.
2792
+ Run: npx -y @floomhq/floom login
2793
+ More help: ${HELP}`;
2794
+ }
2795
+ if (code === "TOKEN_EXPIRED") {
2796
+ return `Your session has expired.
2797
+ Run: npx -y @floomhq/floom login
2798
+ More help: ${HELP}`;
2799
+ }
2800
+ if (code === "SKILL_ACCESS_DENIED" || status === 403) {
2801
+ return `Access denied. This skill belongs to another workspace or you don't have permission to modify it.
2802
+ If you want your own copy, push it with a different slug. More help: ${HELP}`;
2803
+ }
2804
+ if (code === "SKILL_NOT_FOUND" || status === 404) {
2805
+ return `Skill not found. It may have been deleted or the slug is wrong.
2806
+ Run: npx -y @floomhq/floom list
2807
+ More help: ${HELP}`;
2808
+ }
2809
+ if (code === "RATE_LIMITED" || status === 429) {
2810
+ return `Too many requests. Wait a moment and try again.
2811
+ More help: ${HELP}`;
2812
+ }
2813
+ if (code === "CLI_VERSION_TOO_OLD") {
2814
+ return `Your CLI is out of date.
2815
+ Run: npx -y @floomhq/floom@latest <command>
2816
+ More help: ${HELP}`;
2817
+ }
2818
+ if (code === "FILE_TOO_LARGE" || code === "BUNDLE_TOO_LARGE") {
2819
+ return `${raw}
2820
+ Reduce the size of your skill files before pushing. More help: ${HELP}`;
2821
+ }
2822
+ if (status && status >= 500) {
2823
+ return `Floom service error (${status}). Please try again in a moment.
2824
+ More help: ${HELP}`;
2825
+ }
2826
+ return raw;
2827
+ }
2742
2828
  async function api(path, opts = {}) {
2743
2829
  const auth = await readAuth();
2744
2830
  const token = process.env.FLOOM_API_TOKEN?.trim() || auth?.token;
2745
2831
  if (opts.authRequired && !token) {
2746
- throw new FloomError("AUTH_REQUIRED", "Not logged in. Run: npx -y @floomhq/floom login");
2832
+ throw new FloomError(
2833
+ "AUTH_REQUIRED",
2834
+ "Not signed in.\n Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2835
+ );
2747
2836
  }
2748
2837
  let lastError = null;
2749
2838
  const bases = getApiBaseUrls(auth?.apiUrl);
@@ -2776,7 +2865,9 @@ async function api(path, opts = {}) {
2776
2865
  } catch (e) {
2777
2866
  lastError = new FloomError(
2778
2867
  "INTERNAL_ERROR",
2779
- `Unable to reach Floom API at ${base}: ${e.message}`,
2868
+ `Cannot reach Floom (${base}): ${e.message}
2869
+ Check your internet connection or firewall. If on a corporate network, you may need to set a proxy: npm config set proxy http://...
2870
+ More help: https://floom.dev/docs#troubleshooting`,
2780
2871
  { apiUrl: base }
2781
2872
  );
2782
2873
  continue;
@@ -2796,17 +2887,19 @@ async function api(path, opts = {}) {
2796
2887
  }
2797
2888
  const envelopeError = json?.error;
2798
2889
  if (envelopeError && typeof envelopeError === "object" && envelopeError.code) {
2890
+ const rawMsg2 = typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code);
2799
2891
  throw new FloomError(
2800
2892
  envelopeError.code,
2801
- typeof envelopeError.message === "string" ? envelopeError.message : String(envelopeError.code),
2893
+ friendlyApiErrorMessage(envelopeError.code, rawMsg2, res.status),
2802
2894
  { status: res.status, requestId: envelopeError.request_id, apiUrl: base }
2803
2895
  );
2804
2896
  }
2805
2897
  if (res.ok) return json;
2806
2898
  const err = envelopeError ?? {};
2899
+ const rawMsg = err.message ?? `HTTP ${res.status} ${res.statusText}`;
2807
2900
  lastError = new FloomError(
2808
2901
  err.code ?? "INTERNAL_ERROR",
2809
- err.message ?? `HTTP ${res.status} ${res.statusText}`,
2902
+ friendlyApiErrorMessage(err.code ?? "INTERNAL_ERROR", rawMsg, res.status),
2810
2903
  { status: res.status, requestId: err.request_id, apiUrl: base }
2811
2904
  );
2812
2905
  if (res.status !== 404 || json?.error) break;
@@ -2862,7 +2955,9 @@ async function loginCommand() {
2862
2955
  if (remaining <= 0) break;
2863
2956
  await sleep(Math.min(interval, remaining));
2864
2957
  }
2865
- throw new Error("Login timed out.");
2958
+ throw new Error(
2959
+ "Login timed out.\n The browser approval window expired. Run: npx -y @floomhq/floom login\n More help: https://floom.dev/docs#troubleshooting"
2960
+ );
2866
2961
  }
2867
2962
 
2868
2963
  // src/commands/mcp.ts
@@ -2936,7 +3031,9 @@ async function readManifest(root) {
2936
3031
  if (err.code === "ENOENT") return null;
2937
3032
  if (error instanceof SyntaxError || error instanceof ZodError) {
2938
3033
  throw new Error(
2939
- `Corrupted manifest at ${manifestPath(root)}. Remove that file or run: npx -y @floomhq/floom pull`
3034
+ `Corrupted manifest at ${manifestPath(root)}. Remove that file or run floom pull after backing up local edits.
3035
+ Run: rm ${manifestPath(root)} && npx -y @floomhq/floom pull
3036
+ More help: https://floom.dev/docs#troubleshooting`
2940
3037
  );
2941
3038
  }
2942
3039
  throw error;
@@ -3148,8 +3245,11 @@ function printDetectedTargets(targets) {
3148
3245
  for (const target of targets) log.info(`- ${target}`);
3149
3246
  }
3150
3247
  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.`);
3248
+ log.err(`No AI agent recognized on this machine.`);
3249
+ log.info(` Floom looks for: ${INSTALL_TARGETS.join(", ")}.`);
3250
+ log.info(` Make sure one of these is installed, then re-run.`);
3251
+ log.info(` Or pick one explicitly: npx -y @floomhq/floom pull --target claude`);
3252
+ log.info(` More help: https://floom.dev/docs#agents`);
3153
3253
  }
3154
3254
  async function statusCommand(options) {
3155
3255
  if (!options.target) {
@@ -3563,11 +3663,17 @@ async function findImmediateSkillDirs(root) {
3563
3663
  async function pushOneSkill(root, pushApi) {
3564
3664
  const dirStat = await stat4(root).catch(() => null);
3565
3665
  if (!dirStat || !dirStat.isDirectory()) {
3566
- throw new Error(`Directory not found: ${root}`);
3666
+ throw new Error(
3667
+ `Folder not found: ${root}
3668
+ Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
3669
+ );
3567
3670
  }
3568
3671
  const bundle = await collectBundle(root);
3569
3672
  const skillMd = bundle.files.find((file) => file.relPath === "SKILL.md");
3570
- if (!skillMd) throw new Error("SKILL.md is required.");
3673
+ if (!skillMd) throw new Error(
3674
+ `Missing SKILL.md in ${root}.
3675
+ Every skill folder needs a SKILL.md at its root. See: https://floom.dev/docs#concept`
3676
+ );
3571
3677
  const files = await Promise.all(bundle.files.map(async (file) => ({
3572
3678
  path: file.relPath,
3573
3679
  content_base64: (await readFile6(file.absPath)).toString("base64")
@@ -3598,7 +3704,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3598
3704
  const root = resolve2(dir);
3599
3705
  const dirStat = await stat4(root).catch(() => null);
3600
3706
  if (!dirStat || !dirStat.isDirectory()) {
3601
- throw new Error(`Directory not found: ${dir}`);
3707
+ throw new Error(
3708
+ `Folder not found: ${dir}
3709
+ Check the path and try again. Run from your skill folder or pass the path: npx -y @floomhq/floom push ./path/to/skill`
3710
+ );
3602
3711
  }
3603
3712
  const pushApi = deps.pushApi ?? api;
3604
3713
  if (await hasSkillMarkdown(root)) {
@@ -3607,7 +3716,10 @@ async function pushCommand(dir = ".", options = {}, deps = {}) {
3607
3716
  return;
3608
3717
  }
3609
3718
  const skillDirs = await findImmediateSkillDirs(root);
3610
- if (skillDirs.length === 0) throw new Error("SKILL.md is required.");
3719
+ if (skillDirs.length === 0) throw new Error(
3720
+ `No skills found in ${dir}.
3721
+ Neither the folder itself nor any immediate subfolder contains a SKILL.md. See: https://floom.dev/docs#concept`
3722
+ );
3611
3723
  const concurrency = parseConcurrency(options.concurrency);
3612
3724
  const startedAt = Date.now();
3613
3725
  const errors = [];
@@ -3704,6 +3816,221 @@ async function renameMachineCommand(newLabel, _opts) {
3704
3816
  }
3705
3817
  }
3706
3818
 
3819
+ // src/commands/add.ts
3820
+ import { mkdir as mkdir5, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
3821
+ import { join as join8 } from "node:path";
3822
+ var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
3823
+ function parseToken(input) {
3824
+ const trimmed = input.trim();
3825
+ if (TOKEN_RE.test(trimmed)) return trimmed;
3826
+ try {
3827
+ const url = new URL(trimmed);
3828
+ const parts = url.pathname.split("/").filter(Boolean);
3829
+ if (parts.length >= 2 && parts[parts.length - 2] === "s") {
3830
+ const candidate = parts[parts.length - 1];
3831
+ if (candidate && TOKEN_RE.test(candidate)) return candidate;
3832
+ }
3833
+ const last = parts[parts.length - 1];
3834
+ if (last && TOKEN_RE.test(last)) return last;
3835
+ } catch {
3836
+ }
3837
+ return null;
3838
+ }
3839
+ var SAFE_SLUG_RE2 = /^(?=.{1,80}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
3840
+ var MAX_DECODED_FILE_BYTES2 = 2 * 1024 * 1024;
3841
+ function safeSkillSlug2(slug) {
3842
+ if (!slug) throw new Error("Remote skill slug is empty.");
3843
+ if (slug.length > 80) throw new Error(`Remote skill slug "${slug.slice(0, 80)}\u2026" exceeds 80 characters.`);
3844
+ if (!SAFE_SLUG_RE2.test(slug)) throw new Error(`Unsafe remote skill slug: ${slug}`);
3845
+ return slug;
3846
+ }
3847
+ function safeRemotePath2(path) {
3848
+ const normalized = path.replace(/\\/g, "/");
3849
+ if (!normalized || /[\x00-\x1f\x7f]/.test(normalized) || normalized.startsWith("/") || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
3850
+ throw new Error(`Unsafe remote file path: ${path}`);
3851
+ }
3852
+ return normalized;
3853
+ }
3854
+ function bytesForShareFile(file) {
3855
+ if (file.mode === "text") {
3856
+ const text = file.content_text ?? "";
3857
+ if (Buffer.byteLength(text, "utf8") > MAX_DECODED_FILE_BYTES2) {
3858
+ throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
3859
+ }
3860
+ return Buffer.from(text, "utf8");
3861
+ }
3862
+ const url = file.binary?.download_url ?? null;
3863
+ if (!url?.startsWith("data:")) return Buffer.alloc(0);
3864
+ const [, encoded] = url.split(",", 2);
3865
+ const base64 = encoded ?? "";
3866
+ const bytes = Buffer.from(base64, "base64");
3867
+ if (bytes.byteLength > MAX_DECODED_FILE_BYTES2) {
3868
+ throw new Error(`Remote file ${file.path} exceeds the 2 MiB decoded file limit.`);
3869
+ }
3870
+ return bytes;
3871
+ }
3872
+ function derivedSlug(title) {
3873
+ return title.toLowerCase().replace(/[^a-z0-9 -]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 80).replace(/^-+|-+$/g, "") || "shared-skill";
3874
+ }
3875
+ async function fetchPublicShare(token) {
3876
+ const apiBase = getAppUrl().replace(/\/$/, "");
3877
+ const url = `${apiBase}/api/v1/share/${encodeURIComponent(token)}`;
3878
+ const controller = new AbortController();
3879
+ const timer = setTimeout(() => controller.abort(), 3e4);
3880
+ let res;
3881
+ try {
3882
+ res = await fetch(url, {
3883
+ headers: { "User-Agent": "floom-cli/add" },
3884
+ redirect: "manual",
3885
+ signal: controller.signal
3886
+ });
3887
+ } catch (e) {
3888
+ if (e.name === "AbortError") {
3889
+ throw new Error("Request timed out. Check your internet connection and try again.");
3890
+ }
3891
+ throw new Error(`Cannot reach Floom: ${e.message}`);
3892
+ } finally {
3893
+ clearTimeout(timer);
3894
+ }
3895
+ if (res.status === 404) {
3896
+ throw new FloomError(
3897
+ "SKILL_NOT_FOUND",
3898
+ "Share link not found or revoked. Ask the sender for a fresh link."
3899
+ );
3900
+ }
3901
+ if (res.status === 429) {
3902
+ throw new FloomError(
3903
+ "RATE_LIMITED",
3904
+ "Too many requests. Try again in a minute."
3905
+ );
3906
+ }
3907
+ if (!res.ok) {
3908
+ throw new Error(`Floom API returned HTTP ${res.status}. Try again in a moment.`);
3909
+ }
3910
+ let json;
3911
+ try {
3912
+ json = await res.json();
3913
+ } catch {
3914
+ throw new Error("Unexpected response from Floom API. Try again in a moment.");
3915
+ }
3916
+ if (json && typeof json === "object" && "error" in json) {
3917
+ const errObj = json.error;
3918
+ if (errObj) throw new Error(errObj.message ?? "API error");
3919
+ }
3920
+ const candidate = json;
3921
+ if (!candidate || typeof candidate !== "object" || !candidate.skill || !Array.isArray(candidate.file_contents)) {
3922
+ throw new Error("Unexpected response shape from Floom API.");
3923
+ }
3924
+ return candidate;
3925
+ }
3926
+ async function addCommand(input, opts = {}) {
3927
+ const token = parseToken(input);
3928
+ if (!token) {
3929
+ log.err(
3930
+ "That doesn't look like a Floom share link. Should look like: https://floom.dev/s/fls_xxx"
3931
+ );
3932
+ process.exitCode = 1;
3933
+ return;
3934
+ }
3935
+ let target;
3936
+ if (opts.target) {
3937
+ try {
3938
+ target = assertInstallTarget(opts.target);
3939
+ } catch {
3940
+ log.err(`Unknown target "${opts.target}". Use one of: claude, codex, cursor, gemini, opencode`);
3941
+ process.exitCode = 1;
3942
+ return;
3943
+ }
3944
+ } else {
3945
+ const detected = await detectInstalledTargets();
3946
+ if (detected.length === 0) {
3947
+ log.err("No AI agent recognized on this machine.");
3948
+ log.info(" Floom looks for: claude, codex, cursor, gemini, opencode.");
3949
+ log.info(" Or pick one explicitly: npx -y @floomhq/floom add <share-url> --target claude");
3950
+ process.exitCode = 1;
3951
+ return;
3952
+ }
3953
+ target = detected[0];
3954
+ }
3955
+ const resolved = resolveInstallDir({ target, global: true });
3956
+ log.step(`Fetching share\u2026`);
3957
+ let shareData;
3958
+ try {
3959
+ shareData = await fetchPublicShare(token);
3960
+ } catch (e) {
3961
+ if (e instanceof FloomError) {
3962
+ log.err(e.message);
3963
+ } else {
3964
+ log.err(e.message ?? "Unknown error");
3965
+ }
3966
+ process.exitCode = 1;
3967
+ return;
3968
+ }
3969
+ const slug = safeSkillSlug2(derivedSlug(shareData.skill.title));
3970
+ for (const file of shareData.file_contents) {
3971
+ safeRemotePath2(file.path);
3972
+ }
3973
+ const cleanup = installCancellationHandler();
3974
+ const tempDir = join8(resolved.dir, ".floom", "tmp", `${slug}-add-${Date.now()}`);
3975
+ cleanup.trackDir(tempDir);
3976
+ try {
3977
+ await mkdir5(tempDir, { recursive: true });
3978
+ for (const file of shareData.file_contents) {
3979
+ const safePath = safeRemotePath2(file.path);
3980
+ const dest = join8(tempDir, ...safePath.split("/"));
3981
+ const destDir = dest.substring(0, dest.lastIndexOf("/"));
3982
+ if (destDir !== tempDir) await mkdir5(destDir, { recursive: true });
3983
+ await writeFile5(dest, bytesForShareFile(file));
3984
+ }
3985
+ const finalDir = join8(resolved.dir, slug);
3986
+ const replacedDir = join8(resolved.dir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
3987
+ let movedExisting = false;
3988
+ try {
3989
+ const { rename: rename2, rm: rmFs } = await import("node:fs/promises");
3990
+ await mkdir5(resolved.dir, { recursive: true });
3991
+ const { stat: stat5 } = await import("node:fs/promises");
3992
+ let existingDir = false;
3993
+ try {
3994
+ await stat5(finalDir);
3995
+ existingDir = true;
3996
+ } catch {
3997
+ }
3998
+ if (existingDir) {
3999
+ await rename2(finalDir, replacedDir);
4000
+ movedExisting = true;
4001
+ }
4002
+ try {
4003
+ await rename2(tempDir, finalDir);
4004
+ } catch (renameErr) {
4005
+ if (movedExisting) {
4006
+ try {
4007
+ await rename2(replacedDir, finalDir);
4008
+ } catch {
4009
+ }
4010
+ }
4011
+ throw new Error(`Failed to install ${slug}: ${renameErr.message}`);
4012
+ }
4013
+ if (movedExisting) await rmFs(replacedDir, { recursive: true, force: true });
4014
+ } catch (e) {
4015
+ throw e;
4016
+ }
4017
+ log.blank();
4018
+ log.ok(`Installed ${shareData.skill.title} ${shareData.skill.version.display} from ${shareData.skill.owner.name}`);
4019
+ log.info(` \u2192 ${finalDir}`);
4020
+ log.blank();
4021
+ log.info(`Next: in your agent, the skill is now loaded under the slug "${slug}".`);
4022
+ } catch (e) {
4023
+ log.err(e.message ?? "Unknown error");
4024
+ process.exitCode = 1;
4025
+ } finally {
4026
+ cleanup.dispose();
4027
+ try {
4028
+ await rm2(tempDir, { recursive: true, force: true });
4029
+ } catch {
4030
+ }
4031
+ }
4032
+ }
4033
+
3707
4034
  // src/index.ts
3708
4035
  async function logoutCommand() {
3709
4036
  const auth = await readAuth();
@@ -3720,7 +4047,9 @@ async function logoutCommand() {
3720
4047
  async function whoamiCommand() {
3721
4048
  const auth = await readAuth();
3722
4049
  if (!auth) {
3723
- log.info("Not logged in. Run: npx -y @floomhq/floom login");
4050
+ log.err("Not signed in.");
4051
+ log.info(" Run: npx -y @floomhq/floom login");
4052
+ log.info(" More help: https://floom.dev/docs#troubleshooting");
3724
4053
  process.exitCode = 1;
3725
4054
  return;
3726
4055
  }
@@ -3739,12 +4068,9 @@ async function whoamiCommand() {
3739
4068
  }
3740
4069
  log.kv("api url", auth.apiUrl);
3741
4070
  } 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");
4071
+ log.err(error.message);
4072
+ log.warn("Session invalid. Run: npx -y @floomhq/floom login");
4073
+ log.info(" More help: https://floom.dev/docs#troubleshooting");
3748
4074
  process.exitCode = 1;
3749
4075
  }
3750
4076
  }
@@ -3759,13 +4085,14 @@ program.command("pull").description("Pull the whole workspace library.").option(
3759
4085
  program.command("list").description("List workspace skills.").action(listCommand);
3760
4086
  program.command("status").description("Show local workspace sync status.").option("--target <target>", "claude | codex | cursor | gemini | opencode").action(statusCommand);
3761
4087
  program.command("mcp").description("Run the local MCP server over stdio.").action(mcpCommand);
4088
+ 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
4089
  program.command("rename-machine <label>").description('Set the friendly name for THIS machine (e.g. "Office Server", "Travel Mac").').action(renameMachineCommand);
3763
4090
  async function main() {
3764
4091
  try {
3765
4092
  await program.parseAsync(process.argv);
3766
4093
  } catch (e) {
3767
4094
  if (e instanceof FloomError) {
3768
- log.err(`${e.code}: ${e.message}`);
4095
+ log.err(e.message);
3769
4096
  process.exit(1);
3770
4097
  }
3771
4098
  log.err(e.message ?? "Unknown error");
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "2.0.3";
1
+ export const VERSION = "2.0.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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",