@floomhq/floom 1.0.8 → 1.0.9

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
@@ -27,11 +27,12 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
27
27
  - `npx -y @floomhq/floom publish <file.md>` — scan and upload a Markdown file. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, `--skill-version <label>`, and `--share <email>`. `--share` sends the normal link by email; no account is needed to add unlisted or public links.
28
28
  - `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
29
29
  - `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
30
- - `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`. Optional `--setup` connects Claude Code and `--force` replaces an existing local copy.
30
+ - `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>.md`. Optional `--setup` connects Claude Code, `--force` / `--update` replaces an existing local copy, and `--json` prints machine-readable output.
31
+ - `npx -y @floomhq/floom update <url-or-slug>` — install the latest remote skill content, replacing the local copy.
31
32
  - `npx -y @floomhq/floom info <url-or-slug>` — show skill metadata. Optional `--json`.
32
33
  - `npx -y @floomhq/floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
33
34
  - `npx -y @floomhq/floom agent-prompt` — print the sentence to paste into Claude Code or Codex.
34
- - `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`.
35
+ - `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`, `--file <path>`.
35
36
  - `npx -y @floomhq/floom connect` — alias for setup.
36
37
  - `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
37
38
  - `npx -y @floomhq/floom sync` — preview: pull your published, saved, and subscribed library skills into `~/.claude/skills/`.
@@ -42,9 +43,9 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
42
43
  - `npx -y @floomhq/floom library subscribe <slug>` — subscribe to a public or unlisted library so sync can pull it locally.
43
44
  - `npx -y @floomhq/floom move <slug> --folder <path>` — set your local folder override for a saved or library skill.
44
45
  - `npx -y @floomhq/floom delete <url-or-slug>` — delete one of your published skills. Optional `--yes`.
45
- - `npx -y @floomhq/floom doctor` — diagnose your Floom setup.
46
- - `floom whoami` — show the signed-in account.
47
- - `floom logout` — delete local credentials.
46
+ - `npx -y @floomhq/floom doctor` — diagnose your Floom setup. Optional `--json`.
47
+ - `npx -y @floomhq/floom whoami` — show the signed-in account.
48
+ - `npx -y @floomhq/floom logout` — delete local credentials.
48
49
 
49
50
  ## Skill format
50
51
 
@@ -65,7 +66,7 @@ version: 0.1.0
65
66
 
66
67
  Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
67
68
 
68
- `floom sync` and `floom watch` are Version 1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
69
+ `npx -y @floomhq/floom sync` and `npx -y @floomhq/floom watch` are Version 1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
69
70
  The manifest records hashes for files Floom previously wrote. Version 1 sync writes missing files
70
71
  only. Remote updates, existing untracked files, and locally edited tracked files are skipped as
71
72
  conflicts. Symlinks are never followed. To replace a local skill manually, run
package/dist/cli.js CHANGED
@@ -63,14 +63,16 @@ function usage() {
63
63
  }
64
64
  function commandUsage() {
65
65
  const out = `
66
- ${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [flags]")}
67
- ${c.dim("Global install binary:")} ${c.cyan("floom-skills")} ${c.dim("<command> [flags]")}
66
+ ${c.bold("Usage:")} ${c.cyan("npx -y @floomhq/floom")} ${c.dim("<command> [args]")}
67
+ ${c.dim("Global install binary:")} ${c.cyan("floom-skills")} ${c.dim("<command> [args]")}
68
68
 
69
69
  ${c.bold("Commands")}
70
70
  ${c.dim("Skills")}
71
71
  ${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
72
72
  ${c.dim("Alias: install")}
73
- ${c.dim("Flags: --target claude|codex (default: claude), --setup, --force")}
73
+ ${c.dim("Flags: --target claude|codex (default: claude), --setup, --force, --update, --json")}
74
+ ${c.cyan("update")} ${c.dim("<url>")} Install the latest remote skill content, replacing the local copy
75
+ ${c.dim("Alias: add --force")}
74
76
  ${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
75
77
  ${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
76
78
 
@@ -96,10 +98,12 @@ function commandUsage() {
96
98
  ${c.dim("Agent setup")}
97
99
  ${c.cyan("setup")} Configure Claude Code or Codex instructions
98
100
  ${c.dim("Alias: connect")}
99
- ${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
101
+ ${c.dim("Flags: --target claude|codex, --yes, --dry-run, --file <path>")}
102
+ ${c.dim(" From a repo, setup writes that repo's CLAUDE.md/AGENTS.md")}
100
103
  ${c.cyan("agent-prompt")} Print the sentence to paste into your agent
101
104
  ${c.dim("Alias: paste")}
102
105
  ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
106
+ ${c.dim("Flags: --json")}
103
107
 
104
108
  ${c.dim("Advanced")}
105
109
  ${c.cyan("library")} Create, browse, and subscribe to libraries
@@ -160,7 +164,7 @@ function parseFlags(argv) {
160
164
  out.explicitVisibility = true;
161
165
  }
162
166
  else if (a === "--update") {
163
- throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
167
+ throw new FloomError("Publish updates are not available in Floom Version 1.", "Publish as a new skill for now. Use `npx -y @floomhq/floom update <link>` to refresh installed local copies.");
164
168
  }
165
169
  else if (a === "--share" || a.startsWith("--share=")) {
166
170
  const { value, nextIndex } = readFlagValue(argv, i, "--share");
@@ -195,7 +199,7 @@ function parseFlags(argv) {
195
199
  throw new FloomError("`--version` prints the Floom CLI version at the top level.", "For skill version labels, use `--skill-version <label>`.");
196
200
  }
197
201
  else if (a.startsWith("--")) {
198
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
202
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom publish skill.md --type instruction --public`.");
199
203
  }
200
204
  else
201
205
  out.rest.push(a);
@@ -206,7 +210,7 @@ function parseFlags(argv) {
206
210
  throw new FloomError("Too many --share recipients.", "Use 200 email addresses or fewer.");
207
211
  }
208
212
  if (out.visibility === "private") {
209
- throw new FloomError("`--private --share` would email a link recipients cannot open.", "Use `--unlisted --share` for invite emails, or `floom share <slug> --add <email>` for email-gated access after publishing.");
213
+ throw new FloomError("`--private --share` would email a link recipients cannot open.", "Use `--unlisted --share` for invite emails, or `npx -y @floomhq/floom share <slug> --add <email>` for email-gated access after publishing.");
210
214
  }
211
215
  }
212
216
  return out;
@@ -243,22 +247,22 @@ function parseShareFlags(argv) {
243
247
  i = nextIndex;
244
248
  }
245
249
  else if (a.startsWith("--")) {
246
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom share <slug> --add person@example.com`.");
250
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
247
251
  }
248
252
  else if (!out.slug) {
249
253
  out.slug = a;
250
254
  }
251
255
  else {
252
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom share <slug> --add person@example.com`.");
256
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
253
257
  }
254
258
  }
255
259
  if (!out.slug)
256
- throw new FloomError("Missing skill slug.", "Try `floom share <slug> --add person@example.com`.");
260
+ throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
257
261
  if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
258
262
  throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
259
263
  }
260
264
  if (!out.list && out.add.length === 0 && out.remove.length === 0) {
261
- throw new FloomError("Missing share action.", "Try `floom share <slug> --add person@example.com`.");
265
+ throw new FloomError("Missing share action.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
262
266
  }
263
267
  return out;
264
268
  }
@@ -277,10 +281,10 @@ function parseInitArgs(argv) {
277
281
  continue;
278
282
  }
279
283
  if (a.startsWith("--")) {
280
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md --template brand-voice`.");
284
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom init skill.md --template brand-voice`.");
281
285
  }
282
286
  if (file) {
283
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
287
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom init skill.md`.");
284
288
  }
285
289
  file = a;
286
290
  }
@@ -292,10 +296,10 @@ function parseListFlags(argv) {
292
296
  if (a === "--json")
293
297
  out.json = true;
294
298
  else if (a.startsWith("--")) {
295
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
299
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom list --help` for usage.");
296
300
  }
297
301
  else {
298
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
302
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom list --json`.");
299
303
  }
300
304
  }
301
305
  return out;
@@ -306,11 +310,11 @@ function parseInfoFlags(argv) {
306
310
  if (a === "--json")
307
311
  out.json = true;
308
312
  else if (a.startsWith("--"))
309
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
313
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
310
314
  else if (!out.slug)
311
315
  out.slug = a;
312
316
  else
313
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
317
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
314
318
  }
315
319
  return out;
316
320
  }
@@ -319,6 +323,7 @@ function parseAddArgs(argv) {
319
323
  let target;
320
324
  let setup = false;
321
325
  let force = false;
326
+ let json = false;
322
327
  for (let i = 0; i < argv.length; i++) {
323
328
  const a = argv[i] ?? "";
324
329
  if (a === "--target" || a.startsWith("--target=")) {
@@ -332,22 +337,40 @@ function parseAddArgs(argv) {
332
337
  else if (a === "--setup") {
333
338
  setup = true;
334
339
  }
335
- else if (a === "--force") {
340
+ else if (a === "--json") {
341
+ json = true;
342
+ }
343
+ else if (a === "--force" || a === "--update") {
336
344
  force = true;
337
345
  }
338
346
  else if (a.startsWith("--")) {
339
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --setup`.");
347
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
340
348
  }
341
349
  else if (slug) {
342
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --setup`.");
350
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
343
351
  }
344
352
  else
345
353
  slug = a;
346
354
  }
347
355
  if (!slug) {
348
- throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --setup`");
356
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom add <url-or-slug> --setup`");
357
+ }
358
+ if (json && setup) {
359
+ throw new FloomError("Conflicting add flags.", "Use `--json` for machine output or `--setup` for interactive agent setup.");
360
+ }
361
+ return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
362
+ }
363
+ function parseDoctorArgs(argv) {
364
+ const out = { json: false };
365
+ for (const a of argv) {
366
+ if (a === "--json")
367
+ out.json = true;
368
+ else if (a.startsWith("--"))
369
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
370
+ else
371
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
349
372
  }
350
- return target ? { slug, target, setup, force } : { slug, setup, force };
373
+ return out;
351
374
  }
352
375
  function parseSearchFlags(argv) {
353
376
  const out = { json: false };
@@ -370,7 +393,7 @@ function parseSearchFlags(argv) {
370
393
  i = nextIndex;
371
394
  }
372
395
  else if (a.startsWith("--")) {
373
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom search \"support tone\" --type instruction`.");
396
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom search \"support tone\" --type instruction`.");
374
397
  }
375
398
  else {
376
399
  terms.push(a);
@@ -385,11 +408,11 @@ function parseDeleteFlags(argv) {
385
408
  if (a === "--yes" || a === "-y")
386
409
  out.yes = true;
387
410
  else if (a.startsWith("--"))
388
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
411
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
389
412
  else if (!out.slug)
390
413
  out.slug = a;
391
414
  else
392
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
415
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
393
416
  }
394
417
  return out;
395
418
  }
@@ -423,12 +446,12 @@ function parseSetupFlags(argv) {
423
446
  i = nextIndex;
424
447
  }
425
448
  else if (a.startsWith("--")) {
426
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom setup --target codex --dry-run`.");
449
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom setup --target codex --dry-run`.");
427
450
  }
428
451
  else if (!out.file)
429
452
  out.file = a;
430
453
  else
431
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom setup --target claude --yes`.");
454
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom setup --target claude --yes`.");
432
455
  }
433
456
  return out;
434
457
  }
@@ -487,17 +510,21 @@ function parseLibraryCreateFlags(argv) {
487
510
  else if (a === "--unlisted")
488
511
  out.visibility = "unlisted";
489
512
  else if (a.startsWith("--")) {
490
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
513
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
491
514
  }
492
515
  else if (!out.slug)
493
516
  out.slug = a;
494
517
  else
495
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom library create <slug> --name <name>`.");
518
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom library create <slug> --name <name>`.");
496
519
  }
497
520
  return out;
498
521
  }
499
522
  async function runLibrary(argv) {
500
523
  const [subcommand, ...rest] = argv;
524
+ if (!subcommand || subcommand === "--json") {
525
+ await libraryList(parseListFlags(argv));
526
+ return;
527
+ }
501
528
  switch (subcommand ?? "list") {
502
529
  case "list": {
503
530
  const flags = parseListFlags(rest);
@@ -507,9 +534,9 @@ async function runLibrary(argv) {
507
534
  case "create": {
508
535
  const flags = parseLibraryCreateFlags(rest);
509
536
  if (!flags.slug)
510
- throw new FloomError("Missing library slug.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
537
+ throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
511
538
  if (!flags.name)
512
- throw new FloomError("Missing --name.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
539
+ throw new FloomError("Missing --name.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
513
540
  await libraryCreate({
514
541
  slug: flags.slug,
515
542
  name: flags.name,
@@ -522,10 +549,10 @@ async function runLibrary(argv) {
522
549
  const flags = parseFolderTagFlags(rest);
523
550
  const [librarySlug, skillSlug] = flags.rest;
524
551
  if (!librarySlug || !skillSlug) {
525
- throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
552
+ throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
526
553
  }
527
554
  if (flags.rest.length > 2) {
528
- throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
555
+ throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
529
556
  }
530
557
  await libraryAddSkill({
531
558
  librarySlug,
@@ -539,10 +566,10 @@ async function runLibrary(argv) {
539
566
  case "rm": {
540
567
  const [librarySlug, skillSlug] = rest;
541
568
  if (!librarySlug || !skillSlug) {
542
- throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
569
+ throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
543
570
  }
544
571
  if (rest.length > 2) {
545
- throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
572
+ throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
546
573
  }
547
574
  await libraryRemoveSkill(librarySlug, skillSlug);
548
575
  return;
@@ -550,9 +577,9 @@ async function runLibrary(argv) {
550
577
  case "subscribe": {
551
578
  const slug = rest[0];
552
579
  if (!slug)
553
- throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
580
+ throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library subscribe superpowers`.");
554
581
  if (rest.length > 1) {
555
- throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
582
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library subscribe superpowers`.");
556
583
  }
557
584
  await librarySubscribe(slug);
558
585
  return;
@@ -560,9 +587,9 @@ async function runLibrary(argv) {
560
587
  case "unsubscribe": {
561
588
  const slug = rest[0];
562
589
  if (!slug)
563
- throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
590
+ throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
564
591
  if (rest.length > 1) {
565
- throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
592
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
566
593
  }
567
594
  await libraryUnsubscribe(slug);
568
595
  return;
@@ -585,10 +612,10 @@ function parseWatchFlags(argv) {
585
612
  i = nextIndex;
586
613
  }
587
614
  else if (a.startsWith("--")) {
588
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom watch --interval 60`.");
615
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
589
616
  }
590
617
  else {
591
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom watch --interval 60`.");
618
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
592
619
  }
593
620
  }
594
621
  return out;
@@ -662,10 +689,10 @@ async function main() {
662
689
  // never block on update-notifier
663
690
  }
664
691
  }
665
- // Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
692
+ // Subcommand --help: any rest arg = --help/-h/help → show top-level reference.
666
693
  // Subcommands are simple enough that one help screen is fine for Version 1.
667
694
  if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
668
- usage();
695
+ commandUsage();
669
696
  return;
670
697
  }
671
698
  try {
@@ -679,7 +706,7 @@ async function main() {
679
706
  commandUsage();
680
707
  return;
681
708
  case "commands":
682
- rejectArgs(rest, "Try `floom commands`.");
709
+ rejectArgs(rest, "Try `npx -y @floomhq/floom commands`.");
683
710
  commandUsage();
684
711
  return;
685
712
  case "--version":
@@ -687,16 +714,16 @@ async function main() {
687
714
  process.stdout.write(`${CLI_VERSION}\n`);
688
715
  return;
689
716
  case "login":
690
- rejectArgs(rest, "Try `floom login`.");
717
+ rejectArgs(rest, "Try `npx -y @floomhq/floom login`.");
691
718
  await login();
692
719
  return;
693
720
  case "logout":
694
- rejectArgs(rest, "Try `floom logout`.");
721
+ rejectArgs(rest, "Try `npx -y @floomhq/floom logout`.");
695
722
  await deleteConfig();
696
723
  process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
697
724
  return;
698
725
  case "whoami":
699
- rejectArgs(rest, "Try `floom whoami`.");
726
+ rejectArgs(rest, "Try `npx -y @floomhq/floom whoami`.");
700
727
  await whoami();
701
728
  return;
702
729
  case "init": {
@@ -708,10 +735,10 @@ async function main() {
708
735
  const flags = parseFlags(rest);
709
736
  const file = flags.rest[0];
710
737
  if (!file) {
711
- throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
738
+ throw new FloomError("Missing file argument.", "Try: `npx -y @floomhq/floom publish skill.md`");
712
739
  }
713
740
  if (flags.rest.length > 1) {
714
- throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
741
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `npx -y @floomhq/floom publish skill.md`");
715
742
  }
716
743
  await publish({
717
744
  file,
@@ -725,7 +752,7 @@ async function main() {
725
752
  return;
726
753
  }
727
754
  case "scan": {
728
- const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
755
+ const file = parseSingleFileArg(rest, "Try `npx -y @floomhq/floom scan skill.md`.");
729
756
  await scanSkill(file);
730
757
  return;
731
758
  }
@@ -754,7 +781,7 @@ async function main() {
754
781
  case "search": {
755
782
  const flags = parseSearchFlags(rest);
756
783
  if (!flags.query) {
757
- throw new FloomError("Missing search query.", "Try: `floom search \"support tone\"`.");
784
+ throw new FloomError("Missing search query.", "Try: `npx -y @floomhq/floom search \"support tone\"`.");
758
785
  }
759
786
  await search({
760
787
  query: flags.query,
@@ -771,6 +798,20 @@ async function main() {
771
798
  ...(flags.target ? { target: flags.target } : {}),
772
799
  setup: flags.setup,
773
800
  force: flags.force,
801
+ json: flags.json,
802
+ });
803
+ if (flags.setup) {
804
+ await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
805
+ }
806
+ return;
807
+ }
808
+ case "update": {
809
+ const flags = parseAddArgs(rest);
810
+ await install(flags.slug, {
811
+ ...(flags.target ? { target: flags.target } : {}),
812
+ setup: flags.setup,
813
+ force: true,
814
+ json: flags.json,
774
815
  });
775
816
  if (flags.setup) {
776
817
  await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
@@ -778,7 +819,7 @@ async function main() {
778
819
  return;
779
820
  }
780
821
  case "sync":
781
- rejectArgs(rest, "Try `floom sync`.");
822
+ rejectArgs(rest, "Try `npx -y @floomhq/floom sync`.");
782
823
  await sync();
783
824
  return;
784
825
  case "setup":
@@ -789,7 +830,7 @@ async function main() {
789
830
  }
790
831
  case "agent-prompt":
791
832
  case "paste":
792
- rejectArgs(rest, "Try `floom agent-prompt`.");
833
+ rejectArgs(rest, "Try `npx -y @floomhq/floom agent-prompt`.");
793
834
  agentPrompt();
794
835
  return;
795
836
  case "watch": {
@@ -811,27 +852,29 @@ async function main() {
811
852
  const flags = parseFolderTagFlags(rest);
812
853
  const slug = flags.rest[0];
813
854
  if (!slug) {
814
- throw new FloomError("Missing skill slug.", "Try `floom move support-tone --folder support/tone`.");
855
+ throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
815
856
  }
816
857
  if (flags.folder === undefined) {
817
858
  throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
818
859
  }
819
860
  if (flags.rest.length > 1) {
820
- throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
861
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
821
862
  }
822
863
  await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
823
864
  return;
824
865
  }
825
866
  case "mcp":
826
- rejectArgs(rest, "Try `floom mcp`.");
867
+ rejectArgs(rest, "Try `npx -y @floomhq/floom mcp`.");
827
868
  printMcpSetup();
828
869
  return;
829
870
  case "doctor":
830
- rejectArgs(rest, "Try `floom doctor`.");
831
- await doctor();
871
+ {
872
+ const flags = parseDoctorArgs(rest);
873
+ await doctor(flags);
874
+ }
832
875
  return;
833
876
  default:
834
- throw new FloomError(`Unknown command: ${cmd}`, "Run `floom --help` to see available commands.");
877
+ throw new FloomError(`Unknown command: ${cmd}`, "Run `npx -y @floomhq/floom --help` to see available commands.");
835
878
  }
836
879
  }
837
880
  catch (e) {
package/dist/delete.js CHANGED
@@ -31,10 +31,10 @@ async function confirm(question) {
31
31
  export async function deleteSkill(opts) {
32
32
  const slug = slugFromInput(opts.slug);
33
33
  if (!slug)
34
- throw new FloomError("Missing skill slug.", "Try: `floom delete <slug>`");
34
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom delete <slug>`");
35
35
  const cfg = await readConfig();
36
36
  if (!cfg)
37
- throw new FloomError("Not signed in.", "Run `floom login` first.");
37
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
38
38
  if (!opts.yes) {
39
39
  process.stdout.write(`\n${symbols.bullet} About to delete ${c.bold(slug)}.\n`);
40
40
  const ok = await confirm(`Delete ${c.bold(slug)}? Cannot be undone in CLI.`);
package/dist/doctor.js CHANGED
@@ -32,7 +32,7 @@ async function checkAuth() {
32
32
  name: "Auth",
33
33
  status: "fail",
34
34
  detail: "Token rejected (401).",
35
- hint: "Run `floom logout && floom login` to refresh.",
35
+ hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to refresh.",
36
36
  };
37
37
  }
38
38
  if (!res.ok) {
@@ -88,7 +88,7 @@ async function checkMcp() {
88
88
  return {
89
89
  name: "MCP",
90
90
  status: "ok",
91
- detail: "Optional MCP not registered. `floom add` still writes local skill files.",
91
+ detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
92
92
  };
93
93
  }
94
94
  return {
@@ -130,7 +130,7 @@ async function checkTargetDir() {
130
130
  name: "Target dir",
131
131
  status: "warn",
132
132
  detail: `${dir} does not exist yet.`,
133
- hint: "It will be created on first `floom add`.",
133
+ hint: "It will be created on first `npx -y @floomhq/floom add`.",
134
134
  };
135
135
  }
136
136
  throw err;
@@ -146,7 +146,7 @@ async function checkLastSync() {
146
146
  name: "Last sync",
147
147
  status: "warn",
148
148
  detail: "No synced skills found yet.",
149
- hint: "Run `floom add <link>` to install your first skill.",
149
+ hint: "Run `npx -y @floomhq/floom add <link>` to install your first skill.",
150
150
  };
151
151
  }
152
152
  // Find most recently modified entry
@@ -239,8 +239,7 @@ async function checkVersion() {
239
239
  };
240
240
  }
241
241
  }
242
- export async function doctor() {
243
- process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
242
+ export async function doctor(opts = {}) {
244
243
  const checks = await Promise.all([
245
244
  checkAuth(),
246
245
  checkMcp(),
@@ -248,6 +247,22 @@ export async function doctor() {
248
247
  checkLastSync(),
249
248
  checkVersion(),
250
249
  ]);
250
+ const anyFail = checks.some((c) => c.status === "fail");
251
+ const anyWarn = checks.some((c) => c.status === "warn");
252
+ const status = anyFail ? "fail" : anyWarn ? "warn" : "ok";
253
+ if (opts.json) {
254
+ process.stdout.write(`${JSON.stringify({
255
+ ok: !anyFail,
256
+ status,
257
+ version: CLI_VERSION,
258
+ config_path: CONFIG_PATH,
259
+ checks,
260
+ }, null, 2)}\n`);
261
+ if (anyFail)
262
+ process.exit(1);
263
+ return;
264
+ }
265
+ process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
251
266
  // Compute column widths for clean table output.
252
267
  const nameW = Math.max(...checks.map((c) => c.name.length), 6);
253
268
  for (const check of checks) {
@@ -257,8 +272,6 @@ export async function doctor() {
257
272
  process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
258
273
  }
259
274
  }
260
- const anyFail = checks.some((c) => c.status === "fail");
261
- const anyWarn = checks.some((c) => c.status === "warn");
262
275
  process.stdout.write("\n");
263
276
  if (anyFail) {
264
277
  process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
package/dist/errors.js CHANGED
@@ -14,7 +14,7 @@ export class FloomError extends Error {
14
14
  }
15
15
  export function friendlyHttp(status, action) {
16
16
  if (status === 401) {
17
- return new FloomError("Your token expired.", "Run `floom login` to refresh.");
17
+ return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
18
18
  }
19
19
  if (status === 403) {
20
20
  return new FloomError(`You don't have permission to ${action}.`);
@@ -23,7 +23,7 @@ export function friendlyHttp(status, action) {
23
23
  if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
24
24
  return new FloomError("Skill not found.", "Check the link or slug, then try again.");
25
25
  }
26
- return new FloomError("Skill not found.", "Run `floom publish` without `--update` to create a new one.");
26
+ return new FloomError("Skill not found.", "Publish as a new skill for now. Publisher-side version updates are planned for a later release.");
27
27
  }
28
28
  if (status === 413) {
29
29
  return new FloomError("That file is too large to publish.");
package/dist/info.js CHANGED
@@ -19,7 +19,7 @@ function slugFromInput(input) {
19
19
  export async function info(opts) {
20
20
  const slug = slugFromInput(opts.slug);
21
21
  if (!slug)
22
- throw new FloomError("Missing skill slug.", "Try: `floom info <slug>`");
22
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom info <slug>`");
23
23
  if (!SLUG_RE.test(slug)) {
24
24
  throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
25
25
  }
@@ -28,7 +28,7 @@ export async function info(opts) {
28
28
  const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
29
29
  let detail;
30
30
  try {
31
- detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "load skill metadata", cfg?.accessToken);
31
+ detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "info skill metadata", cfg?.accessToken);
32
32
  }
33
33
  finally {
34
34
  spinner?.stop();
package/dist/install.js CHANGED
@@ -58,7 +58,7 @@ async function localHash(path) {
58
58
  if (code === "ENOENT")
59
59
  return null;
60
60
  if (code === "ELOOP")
61
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
61
+ throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
62
62
  if (code === "ENOTDIR" || code === "EISDIR") {
63
63
  throw new FloomError("Local path is blocked by an existing file or directory.");
64
64
  }
@@ -144,7 +144,7 @@ async function ensureSafeParentDirectory(root, target) {
144
144
  async function assertSafeDirectory(path) {
145
145
  const stat = await lstat(path);
146
146
  if (stat.isSymbolicLink()) {
147
- throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `floom add` again.");
147
+ throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
148
148
  }
149
149
  if (!stat.isDirectory()) {
150
150
  throw new FloomError("Local path is blocked by an existing file or directory.");
@@ -159,7 +159,7 @@ export async function install(slugInput, opts = {}) {
159
159
  }
160
160
  const cfg = await readConfig();
161
161
  const apiUrl = resolveApiUrl(cfg);
162
- const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
162
+ const spinner = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
163
163
  let detail;
164
164
  try {
165
165
  detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
@@ -168,7 +168,7 @@ export async function install(slugInput, opts = {}) {
168
168
  }
169
169
  }
170
170
  catch (err) {
171
- spinner.stop();
171
+ spinner?.stop();
172
172
  throw err;
173
173
  }
174
174
  try {
@@ -194,7 +194,7 @@ export async function install(slugInput, opts = {}) {
194
194
  catch (err) {
195
195
  const code = err.code;
196
196
  if (code === "ELOOP")
197
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
197
+ throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
198
198
  throw err;
199
199
  }
200
200
  action = "updated";
@@ -212,16 +212,28 @@ export async function install(slugInput, opts = {}) {
212
212
  throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
213
213
  }
214
214
  if (code === "ELOOP") {
215
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
215
+ throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
216
216
  }
217
217
  if (code === "ENOENT") {
218
- throw new FloomError("Local path changed during install.", "Run `floom add` again.");
218
+ throw new FloomError("Local path changed during install.", "Run `npx -y @floomhq/floom add` again.");
219
219
  }
220
220
  throw err;
221
221
  }
222
222
  action = "installed";
223
223
  }
224
- spinner.stop();
224
+ const result = {
225
+ slug,
226
+ title: detail.title,
227
+ action,
228
+ target: targetAgent,
229
+ path: target,
230
+ content_hash: remoteHash,
231
+ };
232
+ spinner?.stop();
233
+ if (opts.json) {
234
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
235
+ return;
236
+ }
225
237
  process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
226
238
  process.stdout.write(` ${c.dim(target)}\n\n`);
227
239
  process.stdout.write(` ${c.bold("Next")}\n`);
package/dist/library.js CHANGED
@@ -35,7 +35,7 @@ export async function libraryList(opts = {}) {
35
35
  export async function libraryCreate(opts) {
36
36
  const cfg = await readConfig();
37
37
  if (!cfg)
38
- throw new FloomError("Not signed in.", "Run `floom login` first.");
38
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
39
39
  const apiUrl = resolveApiUrl(cfg);
40
40
  const result = await postJson(`${apiUrl}/api/v1/libraries`, "create library", cfg.accessToken, {
41
41
  slug: opts.slug,
@@ -45,12 +45,12 @@ export async function libraryCreate(opts) {
45
45
  });
46
46
  process.stdout.write(`\n${symbols.ok} Library created: ${c.cyan(result.slug)}\n`);
47
47
  process.stdout.write(` ${c.dim("API:")} ${apiUrl}/api/v1/libraries/${result.slug}\n`);
48
- process.stdout.write(` ${c.dim("Sync:")} floom library subscribe ${result.slug}\n\n`);
48
+ process.stdout.write(` ${c.dim("Sync:")} npx -y @floomhq/floom library subscribe ${result.slug}\n\n`);
49
49
  }
50
50
  export async function libraryAddSkill(opts) {
51
51
  const cfg = await readConfig();
52
52
  if (!cfg)
53
- throw new FloomError("Not signed in.", "Run `floom login` first.");
53
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
54
54
  const apiUrl = resolveApiUrl(cfg);
55
55
  await postJson(`${apiUrl}/api/v1/libraries/${encodeURIComponent(opts.librarySlug)}/skills`, "add skill to library", cfg.accessToken, {
56
56
  skill_slug: opts.skillSlug,
@@ -66,7 +66,7 @@ export async function libraryAddSkill(opts) {
66
66
  export async function libraryRemoveSkill(librarySlug, skillSlug) {
67
67
  const cfg = await readConfig();
68
68
  if (!cfg)
69
- throw new FloomError("Not signed in.", "Run `floom login` first.");
69
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
70
70
  const apiUrl = resolveApiUrl(cfg);
71
71
  await deleteRequest(`${apiUrl}/api/v1/libraries/${encodeURIComponent(librarySlug)}/skills/${encodeURIComponent(skillSlug)}`, "remove skill from library", cfg.accessToken);
72
72
  process.stdout.write(`\n${symbols.ok} Removed ${c.cyan(skillSlug)} from ${c.cyan(librarySlug)}\n\n`);
@@ -74,7 +74,7 @@ export async function libraryRemoveSkill(librarySlug, skillSlug) {
74
74
  export async function librarySubscribe(slug) {
75
75
  const cfg = await readConfig();
76
76
  if (!cfg)
77
- throw new FloomError("Not signed in.", "Run `floom login` first.");
77
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
78
78
  const apiUrl = resolveApiUrl(cfg);
79
79
  await postJson(`${apiUrl}/api/v1/me/subscriptions`, "subscribe to library", cfg.accessToken, { library_slug: slug });
80
80
  process.stdout.write(`\n${symbols.ok} Subscribed to ${c.cyan(slug)}\n`);
@@ -83,7 +83,7 @@ export async function librarySubscribe(slug) {
83
83
  export async function libraryUnsubscribe(slug) {
84
84
  const cfg = await readConfig();
85
85
  if (!cfg)
86
- throw new FloomError("Not signed in.", "Run `floom login` first.");
86
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
87
87
  const apiUrl = resolveApiUrl(cfg);
88
88
  await deleteRequest(`${apiUrl}/api/v1/me/subscriptions/${encodeURIComponent(slug)}`, "unsubscribe from library", cfg.accessToken);
89
89
  process.stdout.write(`\n${symbols.ok} Unsubscribed from ${c.cyan(slug)}\n\n`);
@@ -91,7 +91,7 @@ export async function libraryUnsubscribe(slug) {
91
91
  export async function moveSkill(opts) {
92
92
  const cfg = await readConfig();
93
93
  if (!cfg)
94
- throw new FloomError("Not signed in.", "Run `floom login` first.");
94
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
95
95
  const apiUrl = resolveApiUrl(cfg);
96
96
  await putJson(`${apiUrl}/api/v1/me/skills/${encodeURIComponent(opts.slug)}/override`, "set skill override", cfg.accessToken, { folder: opts.folder, tags: opts.tags });
97
97
  const folderText = opts.folder ?? c.dim("(root)");
package/dist/list.js CHANGED
@@ -36,7 +36,7 @@ function formatRelative(iso) {
36
36
  export async function list(opts) {
37
37
  const cfg = await readConfig();
38
38
  if (!cfg) {
39
- throw new FloomError("Not signed in.", "Run `floom login` first.");
39
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
40
40
  }
41
41
  const apiUrl = resolveApiUrl(cfg);
42
42
  const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
@@ -54,7 +54,7 @@ export async function list(opts) {
54
54
  }
55
55
  process.stdout.write(`\n${symbols.dot} ${c.bold("Published")} ${c.dim(`(${published.length})`)}\n\n`);
56
56
  if (published.length === 0) {
57
- process.stdout.write(` ${c.dim("Nothing published yet. Try `floom publish skill.md`.")}\n`);
57
+ process.stdout.write(` ${c.dim("Nothing published yet. Try `npx -y @floomhq/floom publish skill.md`.")}\n`);
58
58
  }
59
59
  else {
60
60
  for (const s of published)
package/dist/login.js CHANGED
@@ -21,7 +21,7 @@ export async function login() {
21
21
  catch (err) {
22
22
  spinner.stop();
23
23
  if (err instanceof Error && /timed out/i.test(err.message)) {
24
- throw new FloomError("No worries — try `floom login` again when ready.");
24
+ throw new FloomError("No worries — try `npx -y @floomhq/floom login` again when ready.");
25
25
  }
26
26
  throw err;
27
27
  }
package/dist/mcp.js CHANGED
@@ -2,8 +2,8 @@ import { c } from "./ui.js";
2
2
  export function printMcpSetup() {
3
3
  const snippet = `## Floom
4
4
  - Use Floom skills from the local Floom skills folder when they match the task.
5
- - To install a shared skill, run \`floom add <slug-or-url> --target claude\` or \`floom add <slug-or-url> --target codex\`.
6
- - To find reusable behavior, run \`floom search <query>\`.
5
+ - To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target claude\` or \`npx -y @floomhq/floom add <slug-or-url> --target codex\`.
6
+ - To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
7
7
  - MCP sync is optional preview behavior; use it only while the Floom MCP server is configured and running.`;
8
8
  process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
9
9
  process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
package/dist/publish.js CHANGED
@@ -140,7 +140,7 @@ export async function publish(opts) {
140
140
  const version = opts.version ?? parseVersion(meta.version, "frontmatter version") ?? null;
141
141
  const cfg = await readConfig();
142
142
  if (!cfg) {
143
- throw new FloomError("Not signed in.", "Run `floom login` first.");
143
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
144
144
  }
145
145
  // Later version: detect already-published when --update is missing.
146
146
  // The current API does not return a duplicate-skill code, so we leave
package/dist/secrets.js CHANGED
@@ -5,15 +5,29 @@ const SECRET_PATTERNS = [
5
5
  { label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{25,}\b/g },
6
6
  { label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b/g },
7
7
  { label: "GitHub token", regex: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
8
+ { label: "npm token", regex: /\bnpm_[A-Za-z0-9]{30,}\b/g },
8
9
  { label: "Supabase access token", regex: /\bsbp_[A-Za-z0-9]{30,}\b/g },
9
10
  { label: "Stripe secret key", regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
10
11
  { label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
11
12
  { label: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
13
+ { label: "Database URL with password", regex: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s]{8,}@[^\s]+/gi },
12
14
  { label: "Private key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g },
13
15
  ];
14
16
  const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
15
17
  const PROVIDER_LIKE_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?((?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|sbp_[A-Za-z0-9]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})["']?/gi;
16
- const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|placeholder|replace|changeme|todo|xxx|test|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
18
+ const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|sample|mock|placeholder|replace|changeme|todo|xxx|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
19
+ const PLACEHOLDER_PHRASE_WORDS = new Set([
20
+ "and",
21
+ "fake",
22
+ "is",
23
+ "key",
24
+ "long",
25
+ "looks",
26
+ "real",
27
+ "secret",
28
+ "that",
29
+ "very",
30
+ ]);
17
31
  const PROMPT_INJECTION_PATTERNS = [
18
32
  { label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
19
33
  { label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
@@ -46,6 +60,12 @@ function pushFinding(findings, seen, label, line, value) {
46
60
  seen.add(key);
47
61
  findings.push({ label, line, preview: redact(value) });
48
62
  }
63
+ function isPlaceholderValue(value) {
64
+ if (PLACEHOLDER_RE.test(value))
65
+ return true;
66
+ const words = value.toLowerCase().split(/[^a-z]+/).filter(Boolean);
67
+ return words.length >= 6 && words.every((word) => PLACEHOLDER_PHRASE_WORDS.has(word));
68
+ }
49
69
  export function detectSecrets(input) {
50
70
  const findings = [];
51
71
  const seen = new Set();
@@ -59,14 +79,14 @@ export function detectSecrets(input) {
59
79
  GENERIC_ASSIGNMENT_RE.lastIndex = 0;
60
80
  for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
61
81
  const value = match[1] ?? "";
62
- if (!value || PLACEHOLDER_RE.test(value))
82
+ if (!value || isPlaceholderValue(value))
63
83
  continue;
64
84
  pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
65
85
  }
66
86
  PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
67
87
  for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
68
88
  const value = match[1] ?? "";
69
- if (!value)
89
+ if (!value || isPlaceholderValue(value))
70
90
  continue;
71
91
  pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
72
92
  }
package/dist/setup.js CHANGED
@@ -13,16 +13,16 @@ const TARGETS = {
13
13
  };
14
14
  function floomAgentInstructions(target) {
15
15
  const addCommand = target === "codex"
16
- ? "floom add <slug-or-url> --target codex"
17
- : "floom add <slug-or-url> --target claude";
16
+ ? "npx -y @floomhq/floom add <slug-or-url> --target codex"
17
+ : "npx -y @floomhq/floom add <slug-or-url> --target claude";
18
18
  return `${START_MARKER}
19
19
  ## Floom
20
20
 
21
21
  - Before recreating agent behavior from scratch, check Floom for reusable skills.
22
- - Search or inspect skills with \`floom search <query>\`, \`floom info <slug-or-url>\`, and \`floom list\`.
22
+ - Search or inspect skills with \`npx -y @floomhq/floom search <query>\`, \`npx -y @floomhq/floom info <slug-or-url>\`, and \`npx -y @floomhq/floom list\`.
23
23
  - Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
24
24
  - Use installed Markdown skills from the local skills folder when they match the task.
25
- - \`floom sync\`, \`floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
25
+ - \`npx -y @floomhq/floom sync\`, \`npx -y @floomhq/floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
26
26
  ${END_MARKER}`;
27
27
  }
28
28
  async function fileExists(path) {
@@ -96,7 +96,7 @@ async function detectTarget(opts) {
96
96
  return { agent: "claude", label: TARGETS.claude.label, path: claude };
97
97
  if (codex)
98
98
  return { agent: "codex", label: TARGETS.codex.label, path: codex };
99
- throw new FloomError("No agent instruction file found.", "Run `floom setup --target claude --yes` or `floom setup --target codex --yes` from the repo root.");
99
+ throw new FloomError("No agent instruction file found.", "Run `npx -y @floomhq/floom setup --target claude --yes` or `npx -y @floomhq/floom setup --target codex --yes` from the repo root.");
100
100
  }
101
101
  function renderPreview(target, existing) {
102
102
  const action = existing === null ? "create" : "append";
@@ -108,7 +108,7 @@ function renderPreview(target, existing) {
108
108
  "",
109
109
  floomAgentInstructions(target.agent),
110
110
  "",
111
- `${c.dim("MCP setup guidance:")} run ${c.cyan("floom mcp")} to print local agent commands.`,
111
+ `${c.dim("MCP setup guidance:")} run ${c.cyan("npx -y @floomhq/floom mcp")} to print local agent commands.`,
112
112
  "",
113
113
  ].join("\n");
114
114
  }
@@ -153,7 +153,7 @@ export async function setupAgent(opts) {
153
153
  if (existing === null) {
154
154
  await writeFile(target.path, next, { encoding: "utf8", flag: "wx" }).catch((err) => {
155
155
  if (err instanceof Error && "code" in err && err.code === "EEXIST") {
156
- throw new FloomError("Instruction file appeared while setup was running.", "Re-run `floom setup` so Floom can inspect the current file before writing.");
156
+ throw new FloomError("Instruction file appeared while setup was running.", "Re-run `npx -y @floomhq/floom setup` so Floom can inspect the current file before writing.");
157
157
  }
158
158
  throw err;
159
159
  });
@@ -162,5 +162,5 @@ export async function setupAgent(opts) {
162
162
  await writeFile(target.path, next, "utf8");
163
163
  }
164
164
  process.stdout.write(`\n${symbols.ok} Added Floom instructions to ${c.bold(target.path)}\n`);
165
- process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("floom mcp")}\n\n`);
165
+ process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("npx -y @floomhq/floom mcp")}\n\n`);
166
166
  }
package/dist/share.js CHANGED
@@ -19,11 +19,11 @@ async function readJson(res) {
19
19
  export async function share(opts) {
20
20
  const cfg = await readConfig();
21
21
  if (!cfg) {
22
- throw new FloomError("Not signed in.", "Run `floom login` first.");
22
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
23
23
  }
24
24
  const slug = slugFromInput(opts.slug);
25
25
  if (!slug) {
26
- throw new FloomError("Missing skill slug.", "Try: `floom share <slug> --list`");
26
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom share <slug> --list`");
27
27
  }
28
28
  const apiUrl = resolveApiUrl(cfg);
29
29
  const endpoint = `${apiUrl}/api/skills/${encodeURIComponent(slug)}/share`;
package/dist/sync.js CHANGED
@@ -205,7 +205,7 @@ function conflictError(message, code) {
205
205
  export async function sync(opts = {}) {
206
206
  const cfg = await readConfig();
207
207
  if (!cfg)
208
- throw new FloomError("Not signed in.", "Run `floom login` first.");
208
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
209
209
  await ensureSyncManifestDir();
210
210
  const apiUrl = resolveApiUrl(cfg);
211
211
  const spinner = opts.spinner === false ? null : ora({ text: c.dim("Syncing skills..."), color: "yellow" }).start();
@@ -394,7 +394,7 @@ export async function sync(opts = {}) {
394
394
  process.stderr.write(`${symbols.bullet} [floom] skipped local conflict: ${note}\n`);
395
395
  }
396
396
  if (conflicts > 0) {
397
- process.stderr.write(` ${c.dim("Move or delete the local file, then run `floom sync` again.")}\n`);
397
+ process.stderr.write(` ${c.dim("Move or delete the local file, then run `npx -y @floomhq/floom sync` again.")}\n`);
398
398
  }
399
399
  process.stdout.write(`\n${symbols.ok} [floom] synced ${synced} skills (${unchanged} unchanged, ${updated} updated${conflictNote})${skippedNote}\n\n`);
400
400
  }
package/dist/whoami.js CHANGED
@@ -6,7 +6,7 @@ import { FloomError, friendlyHttp, friendlyNetwork } from "./errors.js";
6
6
  export async function whoami() {
7
7
  const cfg = await readConfig();
8
8
  if (!cfg) {
9
- throw new FloomError("Not signed in.", "Run `floom login` to sign in.");
9
+ throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` to sign in.");
10
10
  }
11
11
  const apiUrl = resolveApiUrl(cfg);
12
12
  const spinner = ora({ text: c.dim("Checking session..."), color: "yellow" }).start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Publish AI skills from your terminal. Share with a link.",
5
5
  "license": "MIT",
6
6
  "type": "module",