@floomhq/floom 1.0.3 → 1.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/README.md CHANGED
@@ -7,6 +7,7 @@ npm install -g @floomhq/floom
7
7
  floom init my-skill.md
8
8
  floom login
9
9
  floom publish my-skill.md
10
+ floom share my-skill --add teammate@example.com
10
11
  floom search review
11
12
  floom add awesome-skill
12
13
  floom setup --target claude --dry-run
@@ -21,6 +22,7 @@ Returns a shareable link like `https://floom.dev/s/ffas93ud`. Anyone with the UR
21
22
  - `floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
22
23
  - `floom init [file.md]` — create a starter skill file.
23
24
  - `floom publish <file.md>` — upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--version <label>`.
25
+ - `floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
24
26
  - `floom list` — show your published skills. Optional `--json`.
25
27
  - `floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`.
26
28
  - `floom info <url-or-slug>` — show skill metadata. Optional `--json`.
package/dist/cli.js CHANGED
@@ -14,79 +14,104 @@ import { sync } from "./sync.js";
14
14
  import { printMcpSetup } from "./mcp.js";
15
15
  import { setupAgent } from "./setup.js";
16
16
  import { search } from "./search.js";
17
+ import { scanSkill } from "./scan.js";
18
+ import { share } from "./share.js";
17
19
  import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
18
20
  import { c, symbols } from "./ui.js";
19
21
  import { printError, FloomError } from "./errors.js";
20
- const VERSION = "1.0.3";
21
- const PKG = { name: "@floomhq/floom", version: VERSION };
22
+ import { CLI_VERSION } from "./version.js";
23
+ const PKG = { name: "@floomhq/floom", version: CLI_VERSION };
22
24
  const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
23
25
  function usage() {
24
26
  const out = `
25
- ${c.coral(" __ _")}
26
- ${c.coral(" / _| |___ ___ _ __")} ${c.dim(`v${VERSION}`)}
27
- ${c.coral("| _| / _ \\/ _ \\ ' \\")}
28
- ${c.coral("|_| |_\\___/\\___/_|_|_|")}
27
+ ${c.coral(" ________")}
28
+ ${c.coral(" / ____/ /___ ____ ____ ___")} ${c.dim(`v${CLI_VERSION}`)}
29
+ ${c.coral(" / /_ / / __ \\/ __ \\/ __ `__ \\")}
30
+ ${c.coral(" / __/ / / /_/ / /_/ / / / / / /")}
31
+ ${c.coral(" /_/ /_/\\____/\\____/_/ /_/ /_/")}
29
32
 
30
33
  ${c.bold("Share AI agent skills with a link.")}
31
34
  ${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
32
35
 
33
- ${c.bold("Usage")}
34
- ${c.cyan("floom")} ${c.dim("<command> [options]")}
36
+ ${c.bold("Do this next")}
37
+ ${c.dim("1. Add a skill someone sent you")}
38
+ ${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --target claude")}
35
39
 
36
- ${c.bold("Start Here")}
37
- ${c.dim("Try a shared skill, no account needed:")}
38
- ${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
40
+ ${c.dim("2. Publish your own skill")}
41
+ ${c.cyan("npx -y @floomhq/floom init")} ${c.dim("support-tone.md")}
42
+ ${c.dim("# edit support-tone.md")}
43
+ ${c.cyan("npx -y @floomhq/floom scan")} ${c.dim("support-tone.md")}
44
+ ${c.cyan("npx -y @floomhq/floom login")}
45
+ ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --type instruction --public")}
39
46
 
40
- ${c.dim("Publish your own skill:")}
41
- ${c.cyan("floom init")} ${c.dim("support-tone.md")}
42
- ${c.cyan("floom login")}
43
- ${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
47
+ ${c.dim("3. Tell your agent where skills land")}
48
+ ${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target claude --yes")}
49
+ ${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target codex --yes")}
44
50
 
45
- ${c.dim("Tell your agent where Floom installs skills:")}
46
- ${c.cyan("floom setup")} ${c.dim("--target claude --dry-run")}
51
+ ${c.bold("Safety")}
52
+ ${c.yellow("!")} ${c.dim("publish scans for API keys, prompt injection, and exfiltration.")}
47
53
 
48
- ${c.bold("Commands")}
49
- ${c.dim("Receive")}
50
- ${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
51
- ${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
52
- ${c.cyan("search")} ${c.dim("<query>")} Search public skills and libraries ${c.dim("[--json]")}
54
+ ${c.bold("More")}
55
+ ${c.cyan("floom commands")} ${c.dim("Full command list")}
56
+ ${c.cyan("floom doctor")} ${c.dim("Check auth, API, and local folders")}
57
+ ${c.dim("Docs")} https://floom.dev
58
+ `;
59
+ process.stdout.write(out);
60
+ }
61
+ function commandUsage() {
62
+ const out = `
63
+ ${c.bold("Usage:")} ${c.cyan("floom")} ${c.dim("<command> [flags]")}
53
64
 
54
- ${c.dim("Create")}
55
- ${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
56
- ${c.cyan("publish")} ${c.dim("<file.md>")} Upload Markdown and print a URL
65
+ ${c.bold("Commands")}
66
+ ${c.dim("Skills")}
67
+ ${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
68
+ ${c.dim("Alias: install")}
69
+ ${c.dim("Flags: --target claude|codex (default: claude)")}
70
+ ${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
71
+ ${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
57
72
 
58
- ${c.dim("Publish Flags")}
59
- ${c.dim("--public | --private | --unlisted")} ${c.dim("Set link visibility")}
60
- ${c.dim("--type <kind>")} ${c.dim("knowledge | instruction | workflow | skill")}
61
- ${c.dim("--installs-as <target>")} ${c.dim("Set install target metadata")}
62
- ${c.dim("claude_skill | memory | rule | codex_instruction")}
63
- ${c.dim("--version <label>")} ${c.dim("Attach a human version label")}
73
+ ${c.dim("Publishing")}
74
+ ${c.cyan("init")} ${c.dim("[path]")} Create a skill scaffold
75
+ ${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
76
+ ${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
77
+ ${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
78
+ ${c.dim(" --skill-version <label>")}
79
+ ${c.cyan("share")} ${c.dim("<slug>")} Email-share one of your skills
80
+ ${c.dim("Flags: --add <email>, --remove <email>, --list")}
64
81
 
65
82
  ${c.dim("Account")}
66
- ${c.cyan("login")} Sign in with Google
67
- ${c.cyan("list")} List your published skills ${c.dim("[--json]")}
68
- ${c.cyan("library")} Create, browse, and subscribe to skill libraries
83
+ ${c.cyan("login")} Authenticate
84
+ ${c.cyan("list")} Your published skills
85
+ ${c.cyan("delete")} ${c.dim("<url>")} Delete one of your skills
86
+ ${c.dim("Alias: rm")}
87
+ ${c.cyan("whoami")} Show the signed-in account
88
+ ${c.cyan("logout")} Switch accounts or remove local credentials
89
+
90
+ ${c.dim("Agent setup")}
91
+ ${c.cyan("setup")} Configure Claude Code or Codex instructions
92
+ ${c.dim("Alias: connect")}
93
+ ${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
94
+ ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
95
+
96
+ ${c.dim("Advanced")}
97
+ ${c.cyan("library")} Create, browse, and subscribe to libraries
98
+ ${c.dim("Alias: lib")}
69
99
  ${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
70
- ${c.cyan("delete")} ${c.dim("<url-or-slug>")} Delete one of your skills ${c.dim("[--yes]")}
71
- ${c.cyan("whoami")} Show the signed-in account
72
- ${c.cyan("logout")} Delete local credentials
100
+ ${c.cyan("mcp")} Print optional MCP setup guidance
101
+ ${c.cyan("sync")} Preview pull of published, saved, and library skills
102
+ ${c.cyan("watch")} Preview polling sync loop
73
103
 
74
- ${c.dim("System")}
75
- ${c.cyan("setup")} ${c.dim("[--target claude|codex] [--file path]")} Add Floom guidance to agent instructions
76
- ${c.cyan("connect")} ${c.dim("[--target claude|codex]")} Alias for setup
77
- ${c.cyan("mcp")} Print MCP setup guidance
78
- ${c.cyan("sync")} Preview: pull published, saved, and library skills
79
- ${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
80
- ${c.cyan("doctor")} Diagnose auth, API, and local setup
81
- ${c.cyan("--help")} Show this help
82
- ${c.cyan("--version")} Show version
104
+ ${c.bold("Examples")}
105
+ ${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
106
+ ${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
107
+ ${c.cyan("floom setup")} ${c.dim("--target claude --yes")}
83
108
 
84
- ${c.bold("Env")}
85
- ${c.cyan("FLOOM_API_URL")} Override the API host
109
+ ${c.bold("Help")}
110
+ ${c.cyan("floom commands")} Show this reference
111
+ ${c.cyan("--help")} Show this reference
112
+ ${c.cyan("--version")} Show CLI version
86
113
 
87
- ${c.bold("Links")}
88
- ${c.dim("Docs")} https://floom.dev
89
- ${c.dim("Source")} https://github.com/floomhq/floom
114
+ ${c.dim("Run")} ${c.cyan("floom")} ${c.dim("with no command for the guided start screen.")}
90
115
  `;
91
116
  process.stdout.write(out);
92
117
  }
@@ -112,14 +137,17 @@ function readFlagValue(argv, index, flag) {
112
137
  }
113
138
  function parseFlags(argv) {
114
139
  const out = { visibility: "unlisted", update: false, rest: [] };
140
+ let visibilityFlag = null;
115
141
  for (let i = 0; i < argv.length; i++) {
116
142
  const a = argv[i] ?? "";
117
- if (a === "--public")
118
- out.visibility = "public";
119
- else if (a === "--private")
120
- out.visibility = "private";
121
- else if (a === "--unlisted")
122
- out.visibility = "unlisted";
143
+ if (a === "--public" || a === "--private" || a === "--unlisted") {
144
+ const nextVisibility = a.slice(2);
145
+ if (visibilityFlag && visibilityFlag !== nextVisibility) {
146
+ throw new FloomError("Conflicting visibility flags.", "Use only one of: --public, --private, or --unlisted.");
147
+ }
148
+ visibilityFlag = nextVisibility;
149
+ out.visibility = nextVisibility;
150
+ }
123
151
  else if (a === "--update") {
124
152
  throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
125
153
  }
@@ -142,19 +170,73 @@ function parseFlags(argv) {
142
170
  out.installsAs = value;
143
171
  i = nextIndex;
144
172
  }
145
- else if (a === "--version" || a.startsWith("--version=")) {
146
- const { value, nextIndex } = readFlagValue(argv, i, "--version");
173
+ else if (a === "--skill-version" || a.startsWith("--skill-version=") || a === "--version" || a.startsWith("--version=")) {
174
+ const flagName = a.startsWith("--skill-version") ? "--skill-version" : "--version";
175
+ const { value, nextIndex } = readFlagValue(argv, i, flagName);
147
176
  if (!VERSION_RE.test(value)) {
148
- throw new FloomError(`Invalid --version: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
177
+ throw new FloomError(`Invalid ${flagName}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
149
178
  }
150
179
  out.version = value;
151
180
  i = nextIndex;
152
181
  }
182
+ else if (a.startsWith("--")) {
183
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
184
+ }
153
185
  else
154
186
  out.rest.push(a);
155
187
  }
156
188
  return out;
157
189
  }
190
+ function parseShareFlags(argv) {
191
+ const out = { list: false, add: [], remove: [] };
192
+ for (let i = 0; i < argv.length; i++) {
193
+ const a = argv[i] ?? "";
194
+ if (a === "--list") {
195
+ out.list = true;
196
+ }
197
+ else if (a === "--add" || a.startsWith("--add=")) {
198
+ const { value, nextIndex } = readFlagValue(argv, i, "--add");
199
+ out.add.push(value);
200
+ i = nextIndex;
201
+ }
202
+ else if (a === "--remove" || a.startsWith("--remove=")) {
203
+ const { value, nextIndex } = readFlagValue(argv, i, "--remove");
204
+ out.remove.push(value);
205
+ i = nextIndex;
206
+ }
207
+ else if (a.startsWith("--")) {
208
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom share <slug> --add person@example.com`.");
209
+ }
210
+ else if (!out.slug) {
211
+ out.slug = a;
212
+ }
213
+ else {
214
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom share <slug> --add person@example.com`.");
215
+ }
216
+ }
217
+ if (!out.slug)
218
+ throw new FloomError("Missing skill slug.", "Try `floom share <slug> --add person@example.com`.");
219
+ if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
220
+ throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
221
+ }
222
+ if (!out.list && out.add.length === 0 && out.remove.length === 0) {
223
+ throw new FloomError("Missing share action.", "Try `floom share <slug> --add person@example.com`.");
224
+ }
225
+ return out;
226
+ }
227
+ function parseInitArgs(argv) {
228
+ let file;
229
+ for (const a of argv) {
230
+ if (a.startsWith("--")) {
231
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md`.");
232
+ }
233
+ if (file) {
234
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
235
+ }
236
+ file = a;
237
+ }
238
+ return file ? { file } : {};
239
+ }
158
240
  function parseListFlags(argv) {
159
241
  const out = { json: false };
160
242
  for (const a of argv) {
@@ -163,6 +245,9 @@ function parseListFlags(argv) {
163
245
  else if (a.startsWith("--")) {
164
246
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
165
247
  }
248
+ else {
249
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
250
+ }
166
251
  }
167
252
  return out;
168
253
  }
@@ -175,9 +260,38 @@ function parseInfoFlags(argv) {
175
260
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
176
261
  else if (!out.slug)
177
262
  out.slug = a;
263
+ else
264
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
178
265
  }
179
266
  return out;
180
267
  }
268
+ function parseAddArgs(argv) {
269
+ let slug;
270
+ let target;
271
+ for (let i = 0; i < argv.length; i++) {
272
+ const a = argv[i] ?? "";
273
+ if (a === "--target" || a.startsWith("--target=")) {
274
+ const { value, nextIndex } = readFlagValue(argv, i, "--target");
275
+ if (value !== "claude" && value !== "codex") {
276
+ throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
277
+ }
278
+ target = value;
279
+ i = nextIndex;
280
+ }
281
+ else if (a.startsWith("--")) {
282
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
283
+ }
284
+ else if (slug) {
285
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
286
+ }
287
+ else
288
+ slug = a;
289
+ }
290
+ if (!slug) {
291
+ throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --target claude`");
292
+ }
293
+ return target ? { slug, target } : { slug };
294
+ }
181
295
  function parseSearchFlags(argv) {
182
296
  const out = { json: false };
183
297
  const terms = [];
@@ -217,9 +331,19 @@ function parseDeleteFlags(argv) {
217
331
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
218
332
  else if (!out.slug)
219
333
  out.slug = a;
334
+ else
335
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
220
336
  }
221
337
  return out;
222
338
  }
339
+ function rejectArgs(argv, usageHint) {
340
+ const arg = argv[0];
341
+ if (!arg)
342
+ return;
343
+ if (arg.startsWith("--"))
344
+ throw new FloomError(`Unknown flag: ${arg}`, usageHint);
345
+ throw new FloomError(`Unexpected argument: ${arg}`, usageHint);
346
+ }
223
347
  function parseSetupFlags(argv) {
224
348
  const out = { dryRun: false, yes: false };
225
349
  for (let i = 0; i < argv.length; i++) {
@@ -343,6 +467,9 @@ async function runLibrary(argv) {
343
467
  if (!librarySlug || !skillSlug) {
344
468
  throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
345
469
  }
470
+ if (flags.rest.length > 2) {
471
+ throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
472
+ }
346
473
  await libraryAddSkill({
347
474
  librarySlug,
348
475
  skillSlug,
@@ -357,6 +484,9 @@ async function runLibrary(argv) {
357
484
  if (!librarySlug || !skillSlug) {
358
485
  throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
359
486
  }
487
+ if (rest.length > 2) {
488
+ throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
489
+ }
360
490
  await libraryRemoveSkill(librarySlug, skillSlug);
361
491
  return;
362
492
  }
@@ -364,6 +494,9 @@ async function runLibrary(argv) {
364
494
  const slug = rest[0];
365
495
  if (!slug)
366
496
  throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
497
+ if (rest.length > 1) {
498
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
499
+ }
367
500
  await librarySubscribe(slug);
368
501
  return;
369
502
  }
@@ -371,6 +504,9 @@ async function runLibrary(argv) {
371
504
  const slug = rest[0];
372
505
  if (!slug)
373
506
  throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
507
+ if (rest.length > 1) {
508
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
509
+ }
374
510
  await libraryUnsubscribe(slug);
375
511
  return;
376
512
  }
@@ -403,6 +539,19 @@ function parseWatchFlags(argv) {
403
539
  function notAvailable(feature) {
404
540
  throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
405
541
  }
542
+ function parseSingleFileArg(argv, usageHint) {
543
+ let file;
544
+ for (const a of argv) {
545
+ if (a.startsWith("--"))
546
+ throw new FloomError(`Unknown flag: ${a}`, usageHint);
547
+ if (file)
548
+ throw new FloomError(`Unexpected argument: ${a}`, usageHint);
549
+ file = a;
550
+ }
551
+ if (!file)
552
+ throw new FloomError("Missing file argument.", usageHint);
553
+ return file;
554
+ }
406
555
  function sleep(ms, signal) {
407
556
  if (signal.aborted)
408
557
  return Promise.resolve();
@@ -458,28 +607,37 @@ async function main() {
458
607
  try {
459
608
  switch (cmd) {
460
609
  case undefined:
610
+ usage();
611
+ return;
461
612
  case "--help":
462
613
  case "-h":
463
614
  case "help":
464
- usage();
615
+ commandUsage();
616
+ return;
617
+ case "commands":
618
+ rejectArgs(rest, "Try `floom commands`.");
619
+ commandUsage();
465
620
  return;
466
621
  case "--version":
467
622
  case "-v":
468
- process.stdout.write(`${VERSION}\n`);
623
+ process.stdout.write(`${CLI_VERSION}\n`);
469
624
  return;
470
625
  case "login":
626
+ rejectArgs(rest, "Try `floom login`.");
471
627
  await login();
472
628
  return;
473
629
  case "logout":
630
+ rejectArgs(rest, "Try `floom logout`.");
474
631
  await deleteConfig();
475
632
  process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
476
633
  return;
477
634
  case "whoami":
635
+ rejectArgs(rest, "Try `floom whoami`.");
478
636
  await whoami();
479
637
  return;
480
638
  case "init": {
481
- const file = rest[0];
482
- await init(file);
639
+ const flags = parseInitArgs(rest);
640
+ await init(flags.file);
483
641
  return;
484
642
  }
485
643
  case "publish": {
@@ -488,6 +646,9 @@ async function main() {
488
646
  if (!file) {
489
647
  throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
490
648
  }
649
+ if (flags.rest.length > 1) {
650
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
651
+ }
491
652
  await publish({
492
653
  file,
493
654
  visibility: flags.visibility,
@@ -498,8 +659,22 @@ async function main() {
498
659
  });
499
660
  return;
500
661
  }
662
+ case "scan": {
663
+ const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
664
+ await scanSkill(file);
665
+ return;
666
+ }
501
667
  case "share":
502
- notAvailable("`floom share`");
668
+ {
669
+ const flags = parseShareFlags(rest);
670
+ if (flags.list) {
671
+ await share({ slug: flags.slug ?? "", kind: "list" });
672
+ }
673
+ else {
674
+ await share({ slug: flags.slug ?? "", kind: "patch", add: flags.add, remove: flags.remove });
675
+ }
676
+ }
677
+ return;
503
678
  case "list": {
504
679
  const flags = parseListFlags(rest);
505
680
  await list(flags);
@@ -526,14 +701,12 @@ async function main() {
526
701
  }
527
702
  case "add":
528
703
  case "install": {
529
- const slug = rest.find((a) => !a.startsWith("--"));
530
- if (!slug) {
531
- throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
532
- }
533
- await install(slug);
704
+ const flags = parseAddArgs(rest);
705
+ await install(flags.slug, flags.target ? { target: flags.target } : {});
534
706
  return;
535
707
  }
536
708
  case "sync":
709
+ rejectArgs(rest, "Try `floom sync`.");
537
710
  await sync();
538
711
  return;
539
712
  case "setup":
@@ -566,13 +739,18 @@ async function main() {
566
739
  if (flags.folder === undefined) {
567
740
  throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
568
741
  }
742
+ if (flags.rest.length > 1) {
743
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
744
+ }
569
745
  await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
570
746
  return;
571
747
  }
572
748
  case "mcp":
749
+ rejectArgs(rest, "Try `floom mcp`.");
573
750
  printMcpSetup();
574
751
  return;
575
752
  case "doctor":
753
+ rejectArgs(rest, "Try `floom doctor`.");
576
754
  await doctor();
577
755
  return;
578
756
  default:
package/dist/config.js CHANGED
@@ -8,6 +8,9 @@ export const DEFAULT_WEB_URL = "https://floom.dev";
8
8
  export function getApiUrl() {
9
9
  return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
10
10
  }
11
+ export function resolveApiUrl(cfg) {
12
+ return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? cfg?.apiUrl?.replace(/\/$/, "") ?? DEFAULT_API_URL;
13
+ }
11
14
  export function getWebUrl() {
12
15
  return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
13
16
  }
package/dist/delete.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import ora from "ora";
4
- import { getApiUrl, readConfig } from "./config.js";
4
+ import { readConfig, resolveApiUrl } from "./config.js";
5
5
  import { deleteRequest } from "./lib/api.js";
6
6
  import { c, symbols } from "./ui.js";
7
7
  import { FloomError } from "./errors.js";
@@ -43,7 +43,7 @@ export async function deleteSkill(opts) {
43
43
  return;
44
44
  }
45
45
  }
46
- const apiUrl = cfg.apiUrl ?? getApiUrl();
46
+ const apiUrl = resolveApiUrl(cfg);
47
47
  const spinner = ora({ text: c.dim(`Deleting ${slug}...`), color: "yellow" }).start();
48
48
  try {
49
49
  await deleteRequest(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "delete skill", cfg.accessToken);
package/dist/doctor.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { stat, readFile, access, readdir, constants } from "node:fs/promises";
4
- import { getApiUrl, readConfig, CONFIG_PATH } from "./config.js";
4
+ import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
5
+ import { floomFetch } from "./lib/api.js";
5
6
  import { c, symbols } from "./ui.js";
6
- const CLI_VERSION = "1.0.2";
7
+ import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
7
8
  function statusBadge(s) {
8
9
  if (s === "ok")
9
10
  return c.green(symbols.ok);
@@ -20,10 +21,11 @@ async function checkAuth() {
20
21
  detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
21
22
  };
22
23
  }
23
- const apiUrl = cfg.apiUrl ?? getApiUrl();
24
+ const apiUrl = resolveApiUrl(cfg);
24
25
  try {
25
- const res = await fetch(`${apiUrl}/api/me`, {
26
- headers: { authorization: `Bearer ${cfg.accessToken}` },
26
+ const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
27
+ token: cfg.accessToken,
28
+ checkOk: false,
27
29
  });
28
30
  if (res.status === 401) {
29
31
  return {
@@ -196,48 +198,49 @@ async function checkLastSync() {
196
198
  }
197
199
  }
198
200
  async function checkVersion() {
199
- const apiUrl = (await readConfig())?.apiUrl ?? getApiUrl();
201
+ const apiUrl = resolveApiUrl(await readConfig());
200
202
  try {
201
- const res = await fetch(`${apiUrl}/api/v1/cli-version`, {
203
+ const res = await floomFetch(`${apiUrl}/api/v1/cli-version`, "check CLI version", {
202
204
  headers: { accept: "application/json" },
205
+ checkOk: false,
203
206
  });
204
207
  if (!res.ok) {
205
208
  // Endpoint optional — treat as info-only, not a failure
206
209
  return {
207
210
  name: "Version",
208
211
  status: "ok",
209
- detail: `CLI v${CLI_VERSION} (server check skipped)`,
212
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)} (server check skipped)`,
210
213
  };
211
214
  }
212
215
  const data = (await res.json());
213
- if (data.min && CLI_VERSION < data.min) {
216
+ if (data.min && compareSemverish(CLI_VERSION, data.min) < 0) {
214
217
  return {
215
218
  name: "Version",
216
219
  status: "fail",
217
- detail: `CLI v${CLI_VERSION} below required v${data.min}.`,
220
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
218
221
  hint: "Run `npm i -g @floomhq/floom` to upgrade.",
219
222
  };
220
223
  }
221
- if (data.latest && CLI_VERSION !== data.latest) {
224
+ if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
222
225
  return {
223
226
  name: "Version",
224
227
  status: "warn",
225
- detail: `CLI v${CLI_VERSION}, latest is v${data.latest}.`,
228
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
226
229
  hint: "Run `npm i -g @floomhq/floom` to upgrade.",
227
230
  };
228
231
  }
229
- return { name: "Version", status: "ok", detail: `CLI v${CLI_VERSION} (current)` };
232
+ return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
230
233
  }
231
234
  catch {
232
235
  return {
233
236
  name: "Version",
234
237
  status: "ok",
235
- detail: `CLI v${CLI_VERSION} (offline)`,
238
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)} (offline)`,
236
239
  };
237
240
  }
238
241
  }
239
242
  export async function doctor() {
240
- process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(v${CLI_VERSION})`)}\n\n`);
243
+ process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
241
244
  const checks = await Promise.all([
242
245
  checkAuth(),
243
246
  checkMcp(),
package/dist/errors.js CHANGED
@@ -40,8 +40,12 @@ export function friendlyNetwork(err) {
40
40
  export function printError(err) {
41
41
  if (err instanceof FloomError) {
42
42
  process.stderr.write(`\n${symbols.fail} ${err.message}\n`);
43
- if (err.hint)
44
- process.stderr.write(` ${c.dim(err.hint)}\n\n`);
43
+ if (err.hint) {
44
+ for (const line of err.hint.split("\n")) {
45
+ process.stderr.write(` ${c.dim(line)}\n`);
46
+ }
47
+ process.stderr.write("\n");
48
+ }
45
49
  else
46
50
  process.stderr.write("\n");
47
51
  return;