@floomhq/floom 1.0.8 → 1.0.10

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,13 @@ 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")}
105
+ ${c.dim("Flags: --target claude|codex")}
102
106
  ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
107
+ ${c.dim("Flags: --json")}
103
108
 
104
109
  ${c.dim("Advanced")}
105
110
  ${c.cyan("library")} Create, browse, and subscribe to libraries
@@ -126,6 +131,7 @@ function commandUsage() {
126
131
  }
127
132
  const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
128
133
  const INIT_TEMPLATES = new Set(["generic", "brand-voice", "pr-review", "sales", "support", "onboarding"]);
134
+ const MAX_SHARE_RECIPIENTS = 25;
129
135
  const INSTALL_TARGETS = new Set([
130
136
  "claude_skill",
131
137
  "memory",
@@ -160,7 +166,7 @@ function parseFlags(argv) {
160
166
  out.explicitVisibility = true;
161
167
  }
162
168
  else if (a === "--update") {
163
- throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
169
+ 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
170
  }
165
171
  else if (a === "--share" || a.startsWith("--share=")) {
166
172
  const { value, nextIndex } = readFlagValue(argv, i, "--share");
@@ -195,18 +201,18 @@ function parseFlags(argv) {
195
201
  throw new FloomError("`--version` prints the Floom CLI version at the top level.", "For skill version labels, use `--skill-version <label>`.");
196
202
  }
197
203
  else if (a.startsWith("--")) {
198
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
204
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom publish skill.md --type instruction --public`.");
199
205
  }
200
206
  else
201
207
  out.rest.push(a);
202
208
  }
203
209
  if (out.shareEmails.length > 0) {
204
210
  out.shareEmails = dedupeEmails(out.shareEmails);
205
- if (out.shareEmails.length > 200) {
206
- throw new FloomError("Too many --share recipients.", "Use 200 email addresses or fewer.");
211
+ if (out.shareEmails.length > MAX_SHARE_RECIPIENTS) {
212
+ throw new FloomError("Too many --share recipients.", `Use ${MAX_SHARE_RECIPIENTS} email addresses or fewer.`);
207
213
  }
208
214
  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.");
215
+ 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
216
  }
211
217
  }
212
218
  return out;
@@ -243,22 +249,22 @@ function parseShareFlags(argv) {
243
249
  i = nextIndex;
244
250
  }
245
251
  else if (a.startsWith("--")) {
246
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom share <slug> --add person@example.com`.");
252
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
247
253
  }
248
254
  else if (!out.slug) {
249
255
  out.slug = a;
250
256
  }
251
257
  else {
252
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom share <slug> --add person@example.com`.");
258
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
253
259
  }
254
260
  }
255
261
  if (!out.slug)
256
- throw new FloomError("Missing skill slug.", "Try `floom share <slug> --add person@example.com`.");
262
+ throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
257
263
  if (out.list && (out.add.length > 0 || out.remove.length > 0)) {
258
264
  throw new FloomError("Conflicting share flags.", "Use --list by itself, or use --add/--remove.");
259
265
  }
260
266
  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`.");
267
+ throw new FloomError("Missing share action.", "Try `npx -y @floomhq/floom share <slug> --add person@example.com`.");
262
268
  }
263
269
  return out;
264
270
  }
@@ -277,10 +283,10 @@ function parseInitArgs(argv) {
277
283
  continue;
278
284
  }
279
285
  if (a.startsWith("--")) {
280
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md --template brand-voice`.");
286
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom init skill.md --template brand-voice`.");
281
287
  }
282
288
  if (file) {
283
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
289
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom init skill.md`.");
284
290
  }
285
291
  file = a;
286
292
  }
@@ -292,10 +298,10 @@ function parseListFlags(argv) {
292
298
  if (a === "--json")
293
299
  out.json = true;
294
300
  else if (a.startsWith("--")) {
295
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
301
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom list --help` for usage.");
296
302
  }
297
303
  else {
298
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
304
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom list --json`.");
299
305
  }
300
306
  }
301
307
  return out;
@@ -306,11 +312,11 @@ function parseInfoFlags(argv) {
306
312
  if (a === "--json")
307
313
  out.json = true;
308
314
  else if (a.startsWith("--"))
309
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
315
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
310
316
  else if (!out.slug)
311
317
  out.slug = a;
312
318
  else
313
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
319
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom info <slug> --json`.");
314
320
  }
315
321
  return out;
316
322
  }
@@ -319,6 +325,7 @@ function parseAddArgs(argv) {
319
325
  let target;
320
326
  let setup = false;
321
327
  let force = false;
328
+ let json = false;
322
329
  for (let i = 0; i < argv.length; i++) {
323
330
  const a = argv[i] ?? "";
324
331
  if (a === "--target" || a.startsWith("--target=")) {
@@ -332,22 +339,61 @@ function parseAddArgs(argv) {
332
339
  else if (a === "--setup") {
333
340
  setup = true;
334
341
  }
335
- else if (a === "--force") {
342
+ else if (a === "--json") {
343
+ json = true;
344
+ }
345
+ else if (a === "--force" || a === "--update") {
336
346
  force = true;
337
347
  }
338
348
  else if (a.startsWith("--")) {
339
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --setup`.");
349
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
340
350
  }
341
351
  else if (slug) {
342
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --setup`.");
352
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom add <url-or-slug> --setup`.");
343
353
  }
344
354
  else
345
355
  slug = a;
346
356
  }
347
357
  if (!slug) {
348
- throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --setup`");
358
+ throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom add <url-or-slug> --setup`");
359
+ }
360
+ if (json && setup) {
361
+ throw new FloomError("Conflicting add flags.", "Use `--json` for machine output or `--setup` for interactive agent setup.");
362
+ }
363
+ return target ? { slug, target, setup, force, json } : { slug, setup, force, json };
364
+ }
365
+ function parseDoctorArgs(argv) {
366
+ const out = { json: false };
367
+ for (const a of argv) {
368
+ if (a === "--json")
369
+ out.json = true;
370
+ else if (a.startsWith("--"))
371
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
372
+ else
373
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom doctor --json`.");
349
374
  }
350
- return target ? { slug, target, setup, force } : { slug, setup, force };
375
+ return out;
376
+ }
377
+ function parseAgentPromptArgs(argv) {
378
+ const out = {};
379
+ for (let i = 0; i < argv.length; i++) {
380
+ const a = argv[i] ?? "";
381
+ if (a === "--target" || a.startsWith("--target=")) {
382
+ const { value, nextIndex } = readFlagValue(argv, i, "--target");
383
+ if (value !== "claude" && value !== "codex") {
384
+ throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
385
+ }
386
+ out.target = value;
387
+ i = nextIndex;
388
+ }
389
+ else if (a.startsWith("--")) {
390
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
391
+ }
392
+ else {
393
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom agent-prompt --target codex`.");
394
+ }
395
+ }
396
+ return out;
351
397
  }
352
398
  function parseSearchFlags(argv) {
353
399
  const out = { json: false };
@@ -370,7 +416,7 @@ function parseSearchFlags(argv) {
370
416
  i = nextIndex;
371
417
  }
372
418
  else if (a.startsWith("--")) {
373
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom search \"support tone\" --type instruction`.");
419
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom search \"support tone\" --type instruction`.");
374
420
  }
375
421
  else {
376
422
  terms.push(a);
@@ -385,11 +431,11 @@ function parseDeleteFlags(argv) {
385
431
  if (a === "--yes" || a === "-y")
386
432
  out.yes = true;
387
433
  else if (a.startsWith("--"))
388
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
434
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
389
435
  else if (!out.slug)
390
436
  out.slug = a;
391
437
  else
392
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
438
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom delete <slug> --yes`.");
393
439
  }
394
440
  return out;
395
441
  }
@@ -423,12 +469,12 @@ function parseSetupFlags(argv) {
423
469
  i = nextIndex;
424
470
  }
425
471
  else if (a.startsWith("--")) {
426
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom setup --target codex --dry-run`.");
472
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom setup --target codex --dry-run`.");
427
473
  }
428
474
  else if (!out.file)
429
475
  out.file = a;
430
476
  else
431
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom setup --target claude --yes`.");
477
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom setup --target claude --yes`.");
432
478
  }
433
479
  return out;
434
480
  }
@@ -487,17 +533,21 @@ function parseLibraryCreateFlags(argv) {
487
533
  else if (a === "--unlisted")
488
534
  out.visibility = "unlisted";
489
535
  else if (a.startsWith("--")) {
490
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
536
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
491
537
  }
492
538
  else if (!out.slug)
493
539
  out.slug = a;
494
540
  else
495
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom library create <slug> --name <name>`.");
541
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom library create <slug> --name <name>`.");
496
542
  }
497
543
  return out;
498
544
  }
499
545
  async function runLibrary(argv) {
500
546
  const [subcommand, ...rest] = argv;
547
+ if (!subcommand || subcommand === "--json") {
548
+ await libraryList(parseListFlags(argv));
549
+ return;
550
+ }
501
551
  switch (subcommand ?? "list") {
502
552
  case "list": {
503
553
  const flags = parseListFlags(rest);
@@ -507,9 +557,9 @@ async function runLibrary(argv) {
507
557
  case "create": {
508
558
  const flags = parseLibraryCreateFlags(rest);
509
559
  if (!flags.slug)
510
- throw new FloomError("Missing library slug.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
560
+ throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
511
561
  if (!flags.name)
512
- throw new FloomError("Missing --name.", "Try `floom library create team-onboarding --name \"Team onboarding\"`.");
562
+ throw new FloomError("Missing --name.", "Try `npx -y @floomhq/floom library create team-onboarding --name \"Team onboarding\"`.");
513
563
  await libraryCreate({
514
564
  slug: flags.slug,
515
565
  name: flags.name,
@@ -522,10 +572,10 @@ async function runLibrary(argv) {
522
572
  const flags = parseFolderTagFlags(rest);
523
573
  const [librarySlug, skillSlug] = flags.rest;
524
574
  if (!librarySlug || !skillSlug) {
525
- throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
575
+ throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
526
576
  }
527
577
  if (flags.rest.length > 2) {
528
- throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
578
+ throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `npx -y @floomhq/floom library add team-onboarding support-tone --folder support`.");
529
579
  }
530
580
  await libraryAddSkill({
531
581
  librarySlug,
@@ -539,10 +589,10 @@ async function runLibrary(argv) {
539
589
  case "rm": {
540
590
  const [librarySlug, skillSlug] = rest;
541
591
  if (!librarySlug || !skillSlug) {
542
- throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
592
+ throw new FloomError("Missing library or skill slug.", "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
543
593
  }
544
594
  if (rest.length > 2) {
545
- throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
595
+ throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `npx -y @floomhq/floom library remove team-onboarding support-tone`.");
546
596
  }
547
597
  await libraryRemoveSkill(librarySlug, skillSlug);
548
598
  return;
@@ -550,9 +600,9 @@ async function runLibrary(argv) {
550
600
  case "subscribe": {
551
601
  const slug = rest[0];
552
602
  if (!slug)
553
- throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
603
+ throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library subscribe superpowers`.");
554
604
  if (rest.length > 1) {
555
- throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
605
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library subscribe superpowers`.");
556
606
  }
557
607
  await librarySubscribe(slug);
558
608
  return;
@@ -560,9 +610,9 @@ async function runLibrary(argv) {
560
610
  case "unsubscribe": {
561
611
  const slug = rest[0];
562
612
  if (!slug)
563
- throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
613
+ throw new FloomError("Missing library slug.", "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
564
614
  if (rest.length > 1) {
565
- throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
615
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `npx -y @floomhq/floom library unsubscribe superpowers`.");
566
616
  }
567
617
  await libraryUnsubscribe(slug);
568
618
  return;
@@ -585,10 +635,10 @@ function parseWatchFlags(argv) {
585
635
  i = nextIndex;
586
636
  }
587
637
  else if (a.startsWith("--")) {
588
- throw new FloomError(`Unknown flag: ${a}`, "Try `floom watch --interval 60`.");
638
+ throw new FloomError(`Unknown flag: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
589
639
  }
590
640
  else {
591
- throw new FloomError(`Unexpected argument: ${a}`, "Try `floom watch --interval 60`.");
641
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `npx -y @floomhq/floom watch --interval 60`.");
592
642
  }
593
643
  }
594
644
  return out;
@@ -609,8 +659,9 @@ function parseSingleFileArg(argv, usageHint) {
609
659
  throw new FloomError("Missing file argument.", usageHint);
610
660
  return file;
611
661
  }
612
- function agentPrompt() {
613
- process.stdout.write("Use my installed Floom skills when they fit the task. Search ~/.claude/skills first.\n");
662
+ function agentPrompt(target = "claude") {
663
+ const folder = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
664
+ process.stdout.write(`Use my installed Floom skills when they fit the task. Search ${folder} first.\n`);
614
665
  }
615
666
  function sleep(ms, signal) {
616
667
  if (signal.aborted)
@@ -662,10 +713,10 @@ async function main() {
662
713
  // never block on update-notifier
663
714
  }
664
715
  }
665
- // Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
716
+ // Subcommand --help: any rest arg = --help/-h/help → show top-level reference.
666
717
  // Subcommands are simple enough that one help screen is fine for Version 1.
667
718
  if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
668
- usage();
719
+ commandUsage();
669
720
  return;
670
721
  }
671
722
  try {
@@ -679,7 +730,7 @@ async function main() {
679
730
  commandUsage();
680
731
  return;
681
732
  case "commands":
682
- rejectArgs(rest, "Try `floom commands`.");
733
+ rejectArgs(rest, "Try `npx -y @floomhq/floom commands`.");
683
734
  commandUsage();
684
735
  return;
685
736
  case "--version":
@@ -687,16 +738,16 @@ async function main() {
687
738
  process.stdout.write(`${CLI_VERSION}\n`);
688
739
  return;
689
740
  case "login":
690
- rejectArgs(rest, "Try `floom login`.");
741
+ rejectArgs(rest, "Try `npx -y @floomhq/floom login`.");
691
742
  await login();
692
743
  return;
693
744
  case "logout":
694
- rejectArgs(rest, "Try `floom logout`.");
745
+ rejectArgs(rest, "Try `npx -y @floomhq/floom logout`.");
695
746
  await deleteConfig();
696
747
  process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
697
748
  return;
698
749
  case "whoami":
699
- rejectArgs(rest, "Try `floom whoami`.");
750
+ rejectArgs(rest, "Try `npx -y @floomhq/floom whoami`.");
700
751
  await whoami();
701
752
  return;
702
753
  case "init": {
@@ -708,10 +759,10 @@ async function main() {
708
759
  const flags = parseFlags(rest);
709
760
  const file = flags.rest[0];
710
761
  if (!file) {
711
- throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
762
+ throw new FloomError("Missing file argument.", "Try: `npx -y @floomhq/floom publish skill.md`");
712
763
  }
713
764
  if (flags.rest.length > 1) {
714
- throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
765
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `npx -y @floomhq/floom publish skill.md`");
715
766
  }
716
767
  await publish({
717
768
  file,
@@ -725,7 +776,7 @@ async function main() {
725
776
  return;
726
777
  }
727
778
  case "scan": {
728
- const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
779
+ const file = parseSingleFileArg(rest, "Try `npx -y @floomhq/floom scan skill.md`.");
729
780
  await scanSkill(file);
730
781
  return;
731
782
  }
@@ -754,7 +805,7 @@ async function main() {
754
805
  case "search": {
755
806
  const flags = parseSearchFlags(rest);
756
807
  if (!flags.query) {
757
- throw new FloomError("Missing search query.", "Try: `floom search \"support tone\"`.");
808
+ throw new FloomError("Missing search query.", "Try: `npx -y @floomhq/floom search \"support tone\"`.");
758
809
  }
759
810
  await search({
760
811
  query: flags.query,
@@ -771,6 +822,20 @@ async function main() {
771
822
  ...(flags.target ? { target: flags.target } : {}),
772
823
  setup: flags.setup,
773
824
  force: flags.force,
825
+ json: flags.json,
826
+ });
827
+ if (flags.setup) {
828
+ await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
829
+ }
830
+ return;
831
+ }
832
+ case "update": {
833
+ const flags = parseAddArgs(rest);
834
+ await install(flags.slug, {
835
+ ...(flags.target ? { target: flags.target } : {}),
836
+ setup: flags.setup,
837
+ force: true,
838
+ json: flags.json,
774
839
  });
775
840
  if (flags.setup) {
776
841
  await setupAgent({ target: flags.target ?? "claude", dryRun: false, yes: true });
@@ -778,7 +843,7 @@ async function main() {
778
843
  return;
779
844
  }
780
845
  case "sync":
781
- rejectArgs(rest, "Try `floom sync`.");
846
+ rejectArgs(rest, "Try `npx -y @floomhq/floom sync`.");
782
847
  await sync();
783
848
  return;
784
849
  case "setup":
@@ -788,10 +853,11 @@ async function main() {
788
853
  return;
789
854
  }
790
855
  case "agent-prompt":
791
- case "paste":
792
- rejectArgs(rest, "Try `floom agent-prompt`.");
793
- agentPrompt();
856
+ case "paste": {
857
+ const flags = parseAgentPromptArgs(rest);
858
+ agentPrompt(flags.target ?? "claude");
794
859
  return;
860
+ }
795
861
  case "watch": {
796
862
  const flags = parseWatchFlags(rest);
797
863
  await watch(flags.intervalSeconds);
@@ -811,27 +877,29 @@ async function main() {
811
877
  const flags = parseFolderTagFlags(rest);
812
878
  const slug = flags.rest[0];
813
879
  if (!slug) {
814
- throw new FloomError("Missing skill slug.", "Try `floom move support-tone --folder support/tone`.");
880
+ throw new FloomError("Missing skill slug.", "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
815
881
  }
816
882
  if (flags.folder === undefined) {
817
883
  throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
818
884
  }
819
885
  if (flags.rest.length > 1) {
820
- throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
886
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `npx -y @floomhq/floom move support-tone --folder support/tone`.");
821
887
  }
822
888
  await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
823
889
  return;
824
890
  }
825
891
  case "mcp":
826
- rejectArgs(rest, "Try `floom mcp`.");
892
+ rejectArgs(rest, "Try `npx -y @floomhq/floom mcp`.");
827
893
  printMcpSetup();
828
894
  return;
829
895
  case "doctor":
830
- rejectArgs(rest, "Try `floom doctor`.");
831
- await doctor();
896
+ {
897
+ const flags = parseDoctorArgs(rest);
898
+ await doctor(flags);
899
+ }
832
900
  return;
833
901
  default:
834
- throw new FloomError(`Unknown command: ${cmd}`, "Run `floom --help` to see available commands.");
902
+ throw new FloomError(`Unknown command: ${cmd}`, "Run `npx -y @floomhq/floom --help` to see available commands.");
835
903
  }
836
904
  }
837
905
  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
@@ -1,4 +1,5 @@
1
1
  import { createServer } from "node:http";
2
+ import { randomBytes } from "node:crypto";
2
3
  import open from "open";
3
4
  import ora from "ora";
4
5
  import { getApiUrl, writeConfig } from "./config.js";
@@ -21,7 +22,7 @@ export async function login() {
21
22
  catch (err) {
22
23
  spinner.stop();
23
24
  if (err instanceof Error && /timed out/i.test(err.message)) {
24
- throw new FloomError("No worries — try `floom login` again when ready.");
25
+ throw new FloomError("No worries — try `npx -y @floomhq/floom login` again when ready.");
25
26
  }
26
27
  throw err;
27
28
  }
@@ -59,6 +60,7 @@ export async function login() {
59
60
  function waitForCallback() {
60
61
  return new Promise((resolve, reject) => {
61
62
  const apiUrl = getApiUrl();
63
+ const state = randomBytes(24).toString("base64url");
62
64
  let settled = false;
63
65
  let retriedEphemeralPort = false;
64
66
  const server = createServer((req, res) => {
@@ -90,6 +92,15 @@ function waitForCallback() {
90
92
  res.end(localCallbackPage("Missing tokens from OAuth response."));
91
93
  return;
92
94
  }
95
+ if (data.state !== state) {
96
+ res.writeHead(400, {
97
+ "access-control-allow-origin": origin,
98
+ "access-control-allow-private-network": "true",
99
+ "content-type": "text/html; charset=utf-8",
100
+ });
101
+ res.end(localCallbackPage("Invalid OAuth state."));
102
+ return;
103
+ }
93
104
  res.writeHead(200, {
94
105
  "access-control-allow-origin": origin,
95
106
  "access-control-allow-private-network": "true",
@@ -143,7 +154,7 @@ function waitForCallback() {
143
154
  return;
144
155
  }
145
156
  const port = address.port;
146
- const target = `${apiUrl}/auth/cli?port=${port}`;
157
+ const target = `${apiUrl}/auth/cli?port=${port}&state=${encodeURIComponent(state)}`;
147
158
  open(target).catch((e) => {
148
159
  const msg = e instanceof Error ? e.message : String(e);
149
160
  process.stdout.write(c.yellow(`Could not auto-open browser (${msg}).\n`) +
@@ -157,7 +168,7 @@ function parseCallbackBody(body, contentType) {
157
168
  if (type.includes("application/x-www-form-urlencoded")) {
158
169
  const params = new URLSearchParams(body);
159
170
  const parsed = {};
160
- for (const key of ["access_token", "refresh_token", "expires_in", "token_type"]) {
171
+ for (const key of ["access_token", "refresh_token", "expires_in", "token_type", "state"]) {
161
172
  const value = params.get(key);
162
173
  if (value)
163
174
  parsed[key] = value;
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 },
@@ -23,8 +37,11 @@ const PROMPT_INJECTION_PATTERNS = [
23
37
  ];
24
38
  const DATA_EXFILTRATION_PATTERNS = [
25
39
  { label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b(?:[^.\n]{0,120})\b(?:to|into) https?:\/\//gi },
40
+ { label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy)\b[^.\n]{0,120}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b[^.\n]{0,120}\b(?:to|into) https?:\/\//gi },
26
41
  { label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
42
+ { label: "Data exfiltration instruction", regex: /\b(?:curl|wget|fetch)\b[^\n]{0,160}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b/gi },
27
43
  { label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
44
+ { label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract)\b[^.\n]{0,120}(?:~\/\.ssh\/[A-Za-z0-9_.-]+|id_rsa|id_ed25519|ssh keys?|private keys?|secret files?)\b/gi },
28
45
  ];
29
46
  function redact(value) {
30
47
  if (value.length <= 12)
@@ -46,6 +63,12 @@ function pushFinding(findings, seen, label, line, value) {
46
63
  seen.add(key);
47
64
  findings.push({ label, line, preview: redact(value) });
48
65
  }
66
+ function isPlaceholderValue(value) {
67
+ if (PLACEHOLDER_RE.test(value))
68
+ return true;
69
+ const words = value.toLowerCase().split(/[^a-z]+/).filter(Boolean);
70
+ return words.length >= 6 && words.every((word) => PLACEHOLDER_PHRASE_WORDS.has(word));
71
+ }
49
72
  export function detectSecrets(input) {
50
73
  const findings = [];
51
74
  const seen = new Set();
@@ -59,14 +82,14 @@ export function detectSecrets(input) {
59
82
  GENERIC_ASSIGNMENT_RE.lastIndex = 0;
60
83
  for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
61
84
  const value = match[1] ?? "";
62
- if (!value || PLACEHOLDER_RE.test(value))
85
+ if (!value || isPlaceholderValue(value))
63
86
  continue;
64
87
  pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
65
88
  }
66
89
  PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
67
90
  for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
68
91
  const value = match[1] ?? "";
69
- if (!value)
92
+ if (!value || isPlaceholderValue(value))
70
93
  continue;
71
94
  pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
72
95
  }
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.10",
4
4
  "description": "Publish AI skills from your terminal. Share with a link.",
5
5
  "license": "MIT",
6
6
  "type": "module",