@floomhq/floom 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -14,79 +14,101 @@ import { sync } from "./sync.js";
14
14
  import { printMcpSetup } from "./mcp.js";
15
15
  import { setupAgent } from "./setup.js";
16
16
  import { search } from "./search.js";
17
+ import { scanSkill } from "./scan.js";
17
18
  import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
18
19
  import { c, symbols } from "./ui.js";
19
20
  import { printError, FloomError } from "./errors.js";
20
- const VERSION = "1.0.3";
21
- const PKG = { name: "@floomhq/floom", version: VERSION };
21
+ import { CLI_VERSION } from "./version.js";
22
+ const PKG = { name: "@floomhq/floom", version: CLI_VERSION };
22
23
  const V1_NOT_AVAILABLE = "Not available in Floom Version 1.";
23
24
  function usage() {
24
25
  const out = `
25
- ${c.coral(" __ _")}
26
- ${c.coral(" / _| |___ ___ _ __")} ${c.dim(`v${VERSION}`)}
27
- ${c.coral("| _| / _ \\/ _ \\ ' \\")}
28
- ${c.coral("|_| |_\\___/\\___/_|_|_|")}
26
+ ${c.coral(" ________")}
27
+ ${c.coral(" / ____/ /___ ____ ____ ___")} ${c.dim(`v${CLI_VERSION}`)}
28
+ ${c.coral(" / /_ / / __ \\/ __ \\/ __ `__ \\")}
29
+ ${c.coral(" / __/ / / /_/ / /_/ / / / / / /")}
30
+ ${c.coral(" /_/ /_/\\____/\\____/_/ /_/ /_/")}
29
31
 
30
32
  ${c.bold("Share AI agent skills with a link.")}
31
33
  ${c.dim("Publish knowledge, instructions, and workflows from your terminal.")}
32
34
 
33
- ${c.bold("Usage")}
34
- ${c.cyan("floom")} ${c.dim("<command> [options]")}
35
+ ${c.bold("Do this next")}
36
+ ${c.dim("1. Add a skill someone sent you")}
37
+ ${c.cyan("npx -y @floomhq/floom add")} ${c.dim("https://floom.dev/s/ffas93ud --target claude")}
35
38
 
36
- ${c.bold("Start Here")}
37
- ${c.dim("Try a shared skill, no account needed:")}
38
- ${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
39
+ ${c.dim("2. Publish your own skill")}
40
+ ${c.cyan("npx -y @floomhq/floom init")} ${c.dim("support-tone.md")}
41
+ ${c.dim("# edit support-tone.md")}
42
+ ${c.cyan("npx -y @floomhq/floom scan")} ${c.dim("support-tone.md")}
43
+ ${c.cyan("npx -y @floomhq/floom login")}
44
+ ${c.cyan("npx -y @floomhq/floom publish")} ${c.dim("support-tone.md --type instruction --public")}
39
45
 
40
- ${c.dim("Publish your own skill:")}
41
- ${c.cyan("floom init")} ${c.dim("support-tone.md")}
42
- ${c.cyan("floom login")}
43
- ${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
46
+ ${c.dim("3. Tell your agent where skills land")}
47
+ ${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target claude --yes")}
48
+ ${c.cyan("npx -y @floomhq/floom setup")} ${c.dim("--target codex --yes")}
44
49
 
45
- ${c.dim("Tell your agent where Floom installs skills:")}
46
- ${c.cyan("floom setup")} ${c.dim("--target claude --dry-run")}
50
+ ${c.bold("Safety")}
51
+ ${c.yellow("!")} ${c.dim("publish scans for API keys, prompt injection, and exfiltration.")}
47
52
 
48
- ${c.bold("Commands")}
49
- ${c.dim("Receive")}
50
- ${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
51
- ${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
52
- ${c.cyan("search")} ${c.dim("<query>")} Search public skills and libraries ${c.dim("[--json]")}
53
+ ${c.bold("More")}
54
+ ${c.cyan("floom commands")} ${c.dim("Full command list")}
55
+ ${c.cyan("floom doctor")} ${c.dim("Check auth, API, and local folders")}
56
+ ${c.dim("Docs")} https://floom.dev
57
+ `;
58
+ process.stdout.write(out);
59
+ }
60
+ function commandUsage() {
61
+ const out = `
62
+ ${c.bold("Usage:")} ${c.cyan("floom")} ${c.dim("<command> [flags]")}
53
63
 
54
- ${c.dim("Create")}
55
- ${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
56
- ${c.cyan("publish")} ${c.dim("<file.md>")} Upload Markdown and print a URL
64
+ ${c.bold("Commands")}
65
+ ${c.dim("Skills")}
66
+ ${c.cyan("add")} ${c.dim("<url>")} Install a skill into the local agent skills folder
67
+ ${c.dim("Alias: install")}
68
+ ${c.dim("Flags: --target claude|codex (default: claude)")}
69
+ ${c.cyan("search")} ${c.dim("<query>")} Find public skills and libraries
70
+ ${c.cyan("info")} ${c.dim("<url>")} Show skill metadata
57
71
 
58
- ${c.dim("Publish Flags")}
59
- ${c.dim("--public | --private | --unlisted")} ${c.dim("Set link visibility")}
60
- ${c.dim("--type <kind>")} ${c.dim("knowledge | instruction | workflow | skill")}
61
- ${c.dim("--installs-as <target>")} ${c.dim("Set install target metadata")}
62
- ${c.dim("claude_skill | memory | rule | codex_instruction")}
63
- ${c.dim("--version <label>")} ${c.dim("Attach a human version label")}
72
+ ${c.dim("Publishing")}
73
+ ${c.cyan("init")} ${c.dim("[path]")} Create a skill scaffold
74
+ ${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
75
+ ${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
76
+ ${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
77
+ ${c.dim(" --skill-version <label>")}
64
78
 
65
79
  ${c.dim("Account")}
66
- ${c.cyan("login")} Sign in with Google
67
- ${c.cyan("list")} List your published skills ${c.dim("[--json]")}
68
- ${c.cyan("library")} Create, browse, and subscribe to skill libraries
80
+ ${c.cyan("login")} Authenticate
81
+ ${c.cyan("list")} Your published skills
82
+ ${c.cyan("delete")} ${c.dim("<url>")} Delete one of your skills
83
+ ${c.dim("Alias: rm")}
84
+ ${c.cyan("whoami")} Show the signed-in account
85
+ ${c.cyan("logout")} Switch accounts or remove local credentials
86
+
87
+ ${c.dim("Agent setup")}
88
+ ${c.cyan("setup")} Configure Claude Code or Codex instructions
89
+ ${c.dim("Alias: connect")}
90
+ ${c.dim("Flags: --target claude|codex, --yes, --dry-run")}
91
+ ${c.cyan("doctor")} Troubleshoot auth, API, and local folders
92
+
93
+ ${c.dim("Advanced")}
94
+ ${c.cyan("library")} Create, browse, and subscribe to libraries
95
+ ${c.dim("Alias: lib")}
69
96
  ${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
70
- ${c.cyan("delete")} ${c.dim("<url-or-slug>")} Delete one of your skills ${c.dim("[--yes]")}
71
- ${c.cyan("whoami")} Show the signed-in account
72
- ${c.cyan("logout")} Delete local credentials
97
+ ${c.cyan("mcp")} Print optional MCP setup guidance
98
+ ${c.cyan("sync")} Preview pull of published, saved, and library skills
99
+ ${c.cyan("watch")} Preview polling sync loop
73
100
 
74
- ${c.dim("System")}
75
- ${c.cyan("setup")} ${c.dim("[--target claude|codex] [--file path]")} Add Floom guidance to agent instructions
76
- ${c.cyan("connect")} ${c.dim("[--target claude|codex]")} Alias for setup
77
- ${c.cyan("mcp")} Print MCP setup guidance
78
- ${c.cyan("sync")} Preview: pull published, saved, and library skills
79
- ${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
80
- ${c.cyan("doctor")} Diagnose auth, API, and local setup
81
- ${c.cyan("--help")} Show this help
82
- ${c.cyan("--version")} Show version
101
+ ${c.bold("Examples")}
102
+ ${c.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
103
+ ${c.cyan("floom publish")} ${c.dim("support-tone.md --type instruction --public")}
104
+ ${c.cyan("floom setup")} ${c.dim("--target claude --yes")}
83
105
 
84
- ${c.bold("Env")}
85
- ${c.cyan("FLOOM_API_URL")} Override the API host
106
+ ${c.bold("Help")}
107
+ ${c.cyan("floom commands")} Show this reference
108
+ ${c.cyan("--help")} Show this reference
109
+ ${c.cyan("--version")} Show CLI version
86
110
 
87
- ${c.bold("Links")}
88
- ${c.dim("Docs")} https://floom.dev
89
- ${c.dim("Source")} https://github.com/floomhq/floom
111
+ ${c.dim("Run")} ${c.cyan("floom")} ${c.dim("with no command for the guided start screen.")}
90
112
  `;
91
113
  process.stdout.write(out);
92
114
  }
@@ -112,14 +134,17 @@ function readFlagValue(argv, index, flag) {
112
134
  }
113
135
  function parseFlags(argv) {
114
136
  const out = { visibility: "unlisted", update: false, rest: [] };
137
+ let visibilityFlag = null;
115
138
  for (let i = 0; i < argv.length; i++) {
116
139
  const a = argv[i] ?? "";
117
- if (a === "--public")
118
- out.visibility = "public";
119
- else if (a === "--private")
120
- out.visibility = "private";
121
- else if (a === "--unlisted")
122
- out.visibility = "unlisted";
140
+ if (a === "--public" || a === "--private" || a === "--unlisted") {
141
+ const nextVisibility = a.slice(2);
142
+ if (visibilityFlag && visibilityFlag !== nextVisibility) {
143
+ throw new FloomError("Conflicting visibility flags.", "Use only one of: --public, --private, or --unlisted.");
144
+ }
145
+ visibilityFlag = nextVisibility;
146
+ out.visibility = nextVisibility;
147
+ }
123
148
  else if (a === "--update") {
124
149
  throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
125
150
  }
@@ -142,19 +167,36 @@ function parseFlags(argv) {
142
167
  out.installsAs = value;
143
168
  i = nextIndex;
144
169
  }
145
- else if (a === "--version" || a.startsWith("--version=")) {
146
- const { value, nextIndex } = readFlagValue(argv, i, "--version");
170
+ else if (a === "--skill-version" || a.startsWith("--skill-version=") || a === "--version" || a.startsWith("--version=")) {
171
+ const flagName = a.startsWith("--skill-version") ? "--skill-version" : "--version";
172
+ const { value, nextIndex } = readFlagValue(argv, i, flagName);
147
173
  if (!VERSION_RE.test(value)) {
148
- throw new FloomError(`Invalid --version: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
174
+ throw new FloomError(`Invalid ${flagName}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
149
175
  }
150
176
  out.version = value;
151
177
  i = nextIndex;
152
178
  }
179
+ else if (a.startsWith("--")) {
180
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
181
+ }
153
182
  else
154
183
  out.rest.push(a);
155
184
  }
156
185
  return out;
157
186
  }
187
+ function parseInitArgs(argv) {
188
+ let file;
189
+ for (const a of argv) {
190
+ if (a.startsWith("--")) {
191
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom init skill.md`.");
192
+ }
193
+ if (file) {
194
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom init skill.md`.");
195
+ }
196
+ file = a;
197
+ }
198
+ return file ? { file } : {};
199
+ }
158
200
  function parseListFlags(argv) {
159
201
  const out = { json: false };
160
202
  for (const a of argv) {
@@ -163,6 +205,9 @@ function parseListFlags(argv) {
163
205
  else if (a.startsWith("--")) {
164
206
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
165
207
  }
208
+ else {
209
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
210
+ }
166
211
  }
167
212
  return out;
168
213
  }
@@ -175,9 +220,38 @@ function parseInfoFlags(argv) {
175
220
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
176
221
  else if (!out.slug)
177
222
  out.slug = a;
223
+ else
224
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
178
225
  }
179
226
  return out;
180
227
  }
228
+ function parseAddArgs(argv) {
229
+ let slug;
230
+ let target;
231
+ for (let i = 0; i < argv.length; i++) {
232
+ const a = argv[i] ?? "";
233
+ if (a === "--target" || a.startsWith("--target=")) {
234
+ const { value, nextIndex } = readFlagValue(argv, i, "--target");
235
+ if (value !== "claude" && value !== "codex") {
236
+ throw new FloomError("Invalid --target.", "Use `claude` or `codex`.");
237
+ }
238
+ target = value;
239
+ i = nextIndex;
240
+ }
241
+ else if (a.startsWith("--")) {
242
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
243
+ }
244
+ else if (slug) {
245
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom add <url-or-slug> --target claude`.");
246
+ }
247
+ else
248
+ slug = a;
249
+ }
250
+ if (!slug) {
251
+ throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug> --target claude`");
252
+ }
253
+ return target ? { slug, target } : { slug };
254
+ }
181
255
  function parseSearchFlags(argv) {
182
256
  const out = { json: false };
183
257
  const terms = [];
@@ -217,9 +291,19 @@ function parseDeleteFlags(argv) {
217
291
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
218
292
  else if (!out.slug)
219
293
  out.slug = a;
294
+ else
295
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
220
296
  }
221
297
  return out;
222
298
  }
299
+ function rejectArgs(argv, usageHint) {
300
+ const arg = argv[0];
301
+ if (!arg)
302
+ return;
303
+ if (arg.startsWith("--"))
304
+ throw new FloomError(`Unknown flag: ${arg}`, usageHint);
305
+ throw new FloomError(`Unexpected argument: ${arg}`, usageHint);
306
+ }
223
307
  function parseSetupFlags(argv) {
224
308
  const out = { dryRun: false, yes: false };
225
309
  for (let i = 0; i < argv.length; i++) {
@@ -343,6 +427,9 @@ async function runLibrary(argv) {
343
427
  if (!librarySlug || !skillSlug) {
344
428
  throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
345
429
  }
430
+ if (flags.rest.length > 2) {
431
+ throw new FloomError(`Unexpected argument: ${flags.rest[2]}`, "Try `floom library add team-onboarding support-tone --folder support`.");
432
+ }
346
433
  await libraryAddSkill({
347
434
  librarySlug,
348
435
  skillSlug,
@@ -357,6 +444,9 @@ async function runLibrary(argv) {
357
444
  if (!librarySlug || !skillSlug) {
358
445
  throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
359
446
  }
447
+ if (rest.length > 2) {
448
+ throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
449
+ }
360
450
  await libraryRemoveSkill(librarySlug, skillSlug);
361
451
  return;
362
452
  }
@@ -364,6 +454,9 @@ async function runLibrary(argv) {
364
454
  const slug = rest[0];
365
455
  if (!slug)
366
456
  throw new FloomError("Missing library slug.", "Try `floom library subscribe superpowers`.");
457
+ if (rest.length > 1) {
458
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library subscribe superpowers`.");
459
+ }
367
460
  await librarySubscribe(slug);
368
461
  return;
369
462
  }
@@ -371,6 +464,9 @@ async function runLibrary(argv) {
371
464
  const slug = rest[0];
372
465
  if (!slug)
373
466
  throw new FloomError("Missing library slug.", "Try `floom library unsubscribe superpowers`.");
467
+ if (rest.length > 1) {
468
+ throw new FloomError(`Unexpected argument: ${rest[1]}`, "Try `floom library unsubscribe superpowers`.");
469
+ }
374
470
  await libraryUnsubscribe(slug);
375
471
  return;
376
472
  }
@@ -403,6 +499,19 @@ function parseWatchFlags(argv) {
403
499
  function notAvailable(feature) {
404
500
  throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
405
501
  }
502
+ function parseSingleFileArg(argv, usageHint) {
503
+ let file;
504
+ for (const a of argv) {
505
+ if (a.startsWith("--"))
506
+ throw new FloomError(`Unknown flag: ${a}`, usageHint);
507
+ if (file)
508
+ throw new FloomError(`Unexpected argument: ${a}`, usageHint);
509
+ file = a;
510
+ }
511
+ if (!file)
512
+ throw new FloomError("Missing file argument.", usageHint);
513
+ return file;
514
+ }
406
515
  function sleep(ms, signal) {
407
516
  if (signal.aborted)
408
517
  return Promise.resolve();
@@ -458,28 +567,37 @@ async function main() {
458
567
  try {
459
568
  switch (cmd) {
460
569
  case undefined:
570
+ usage();
571
+ return;
461
572
  case "--help":
462
573
  case "-h":
463
574
  case "help":
464
- usage();
575
+ commandUsage();
576
+ return;
577
+ case "commands":
578
+ rejectArgs(rest, "Try `floom commands`.");
579
+ commandUsage();
465
580
  return;
466
581
  case "--version":
467
582
  case "-v":
468
- process.stdout.write(`${VERSION}\n`);
583
+ process.stdout.write(`${CLI_VERSION}\n`);
469
584
  return;
470
585
  case "login":
586
+ rejectArgs(rest, "Try `floom login`.");
471
587
  await login();
472
588
  return;
473
589
  case "logout":
590
+ rejectArgs(rest, "Try `floom logout`.");
474
591
  await deleteConfig();
475
592
  process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
476
593
  return;
477
594
  case "whoami":
595
+ rejectArgs(rest, "Try `floom whoami`.");
478
596
  await whoami();
479
597
  return;
480
598
  case "init": {
481
- const file = rest[0];
482
- await init(file);
599
+ const flags = parseInitArgs(rest);
600
+ await init(flags.file);
483
601
  return;
484
602
  }
485
603
  case "publish": {
@@ -488,6 +606,9 @@ async function main() {
488
606
  if (!file) {
489
607
  throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
490
608
  }
609
+ if (flags.rest.length > 1) {
610
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
611
+ }
491
612
  await publish({
492
613
  file,
493
614
  visibility: flags.visibility,
@@ -498,6 +619,11 @@ async function main() {
498
619
  });
499
620
  return;
500
621
  }
622
+ case "scan": {
623
+ const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
624
+ await scanSkill(file);
625
+ return;
626
+ }
501
627
  case "share":
502
628
  notAvailable("`floom share`");
503
629
  case "list": {
@@ -526,14 +652,12 @@ async function main() {
526
652
  }
527
653
  case "add":
528
654
  case "install": {
529
- const slug = rest.find((a) => !a.startsWith("--"));
530
- if (!slug) {
531
- throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
532
- }
533
- await install(slug);
655
+ const flags = parseAddArgs(rest);
656
+ await install(flags.slug, flags.target ? { target: flags.target } : {});
534
657
  return;
535
658
  }
536
659
  case "sync":
660
+ rejectArgs(rest, "Try `floom sync`.");
537
661
  await sync();
538
662
  return;
539
663
  case "setup":
@@ -566,13 +690,18 @@ async function main() {
566
690
  if (flags.folder === undefined) {
567
691
  throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
568
692
  }
693
+ if (flags.rest.length > 1) {
694
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try `floom move support-tone --folder support/tone`.");
695
+ }
569
696
  await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
570
697
  return;
571
698
  }
572
699
  case "mcp":
700
+ rejectArgs(rest, "Try `floom mcp`.");
573
701
  printMcpSetup();
574
702
  return;
575
703
  case "doctor":
704
+ rejectArgs(rest, "Try `floom doctor`.");
576
705
  await doctor();
577
706
  return;
578
707
  default:
package/dist/config.js CHANGED
@@ -8,6 +8,9 @@ export const DEFAULT_WEB_URL = "https://floom.dev";
8
8
  export function getApiUrl() {
9
9
  return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
10
10
  }
11
+ export function resolveApiUrl(cfg) {
12
+ return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? cfg?.apiUrl?.replace(/\/$/, "") ?? DEFAULT_API_URL;
13
+ }
11
14
  export function getWebUrl() {
12
15
  return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
13
16
  }
package/dist/delete.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import ora from "ora";
4
- import { getApiUrl, readConfig } from "./config.js";
4
+ import { readConfig, resolveApiUrl } from "./config.js";
5
5
  import { deleteRequest } from "./lib/api.js";
6
6
  import { c, symbols } from "./ui.js";
7
7
  import { FloomError } from "./errors.js";
@@ -43,7 +43,7 @@ export async function deleteSkill(opts) {
43
43
  return;
44
44
  }
45
45
  }
46
- const apiUrl = cfg.apiUrl ?? getApiUrl();
46
+ const apiUrl = resolveApiUrl(cfg);
47
47
  const spinner = ora({ text: c.dim(`Deleting ${slug}...`), color: "yellow" }).start();
48
48
  try {
49
49
  await deleteRequest(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "delete skill", cfg.accessToken);
package/dist/doctor.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { stat, readFile, access, readdir, constants } from "node:fs/promises";
4
- import { getApiUrl, readConfig, CONFIG_PATH } from "./config.js";
4
+ import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
5
+ import { floomFetch } from "./lib/api.js";
5
6
  import { c, symbols } from "./ui.js";
6
- const CLI_VERSION = "1.0.2";
7
+ import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
7
8
  function statusBadge(s) {
8
9
  if (s === "ok")
9
10
  return c.green(symbols.ok);
@@ -20,10 +21,11 @@ async function checkAuth() {
20
21
  detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
21
22
  };
22
23
  }
23
- const apiUrl = cfg.apiUrl ?? getApiUrl();
24
+ const apiUrl = resolveApiUrl(cfg);
24
25
  try {
25
- const res = await fetch(`${apiUrl}/api/me`, {
26
- headers: { authorization: `Bearer ${cfg.accessToken}` },
26
+ const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
27
+ token: cfg.accessToken,
28
+ checkOk: false,
27
29
  });
28
30
  if (res.status === 401) {
29
31
  return {
@@ -196,48 +198,49 @@ async function checkLastSync() {
196
198
  }
197
199
  }
198
200
  async function checkVersion() {
199
- const apiUrl = (await readConfig())?.apiUrl ?? getApiUrl();
201
+ const apiUrl = resolveApiUrl(await readConfig());
200
202
  try {
201
- const res = await fetch(`${apiUrl}/api/v1/cli-version`, {
203
+ const res = await floomFetch(`${apiUrl}/api/v1/cli-version`, "check CLI version", {
202
204
  headers: { accept: "application/json" },
205
+ checkOk: false,
203
206
  });
204
207
  if (!res.ok) {
205
208
  // Endpoint optional — treat as info-only, not a failure
206
209
  return {
207
210
  name: "Version",
208
211
  status: "ok",
209
- detail: `CLI v${CLI_VERSION} (server check skipped)`,
212
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)} (server check skipped)`,
210
213
  };
211
214
  }
212
215
  const data = (await res.json());
213
- if (data.min && CLI_VERSION < data.min) {
216
+ if (data.min && compareSemverish(CLI_VERSION, data.min) < 0) {
214
217
  return {
215
218
  name: "Version",
216
219
  status: "fail",
217
- detail: `CLI v${CLI_VERSION} below required v${data.min}.`,
220
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
218
221
  hint: "Run `npm i -g @floomhq/floom` to upgrade.",
219
222
  };
220
223
  }
221
- if (data.latest && CLI_VERSION !== data.latest) {
224
+ if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
222
225
  return {
223
226
  name: "Version",
224
227
  status: "warn",
225
- detail: `CLI v${CLI_VERSION}, latest is v${data.latest}.`,
228
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
226
229
  hint: "Run `npm i -g @floomhq/floom` to upgrade.",
227
230
  };
228
231
  }
229
- return { name: "Version", status: "ok", detail: `CLI v${CLI_VERSION} (current)` };
232
+ return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
230
233
  }
231
234
  catch {
232
235
  return {
233
236
  name: "Version",
234
237
  status: "ok",
235
- detail: `CLI v${CLI_VERSION} (offline)`,
238
+ detail: `CLI ${formatVersionLabel(CLI_VERSION)} (offline)`,
236
239
  };
237
240
  }
238
241
  }
239
242
  export async function doctor() {
240
- process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(v${CLI_VERSION})`)}\n\n`);
243
+ process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
241
244
  const checks = await Promise.all([
242
245
  checkAuth(),
243
246
  checkMcp(),
package/dist/errors.js CHANGED
@@ -40,8 +40,12 @@ export function friendlyNetwork(err) {
40
40
  export function printError(err) {
41
41
  if (err instanceof FloomError) {
42
42
  process.stderr.write(`\n${symbols.fail} ${err.message}\n`);
43
- if (err.hint)
44
- process.stderr.write(` ${c.dim(err.hint)}\n\n`);
43
+ if (err.hint) {
44
+ for (const line of err.hint.split("\n")) {
45
+ process.stderr.write(` ${c.dim(line)}\n`);
46
+ }
47
+ process.stderr.write("\n");
48
+ }
45
49
  else
46
50
  process.stderr.write("\n");
47
51
  return;
package/dist/info.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import ora from "ora";
2
- import { getApiUrl, readConfig } from "./config.js";
2
+ import { readConfig, resolveApiUrl } from "./config.js";
3
3
  import { getJson } from "./lib/api.js";
4
4
  import { extractRequires, formatToolList, formatType } from "./lib/skill-labels.js";
5
5
  import { c, symbols } from "./ui.js";
6
6
  import { FloomError } from "./errors.js";
7
+ const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
7
8
  function slugFromInput(input) {
8
9
  const trimmed = input.trim();
9
10
  try {
@@ -19,8 +20,11 @@ export async function info(opts) {
19
20
  const slug = slugFromInput(opts.slug);
20
21
  if (!slug)
21
22
  throw new FloomError("Missing skill slug.", "Try: `floom info <slug>`");
23
+ if (!SLUG_RE.test(slug)) {
24
+ throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
25
+ }
22
26
  const cfg = await readConfig();
23
- const apiUrl = cfg?.apiUrl ?? getApiUrl();
27
+ const apiUrl = resolveApiUrl(cfg);
24
28
  const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
25
29
  let detail;
26
30
  try {
package/dist/init.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { writeFile, access } from "node:fs/promises";
2
- import { resolve, basename } from "node:path";
2
+ import { dirname, resolve, basename } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
5
  import { c, symbols } from "./ui.js";
@@ -43,10 +43,29 @@ export async function init(filename) {
43
43
  return;
44
44
  }
45
45
  }
46
- await writeFile(filePath, TEMPLATE, "utf8");
46
+ try {
47
+ await writeFile(filePath, TEMPLATE, "utf8");
48
+ }
49
+ catch (err) {
50
+ const code = err.code;
51
+ if (code === "ENOENT") {
52
+ throw new FloomError(`Directory not found: ${dirname(target)}`, "Create the directory first, or choose a filename in the current directory.");
53
+ }
54
+ if (code === "EISDIR") {
55
+ throw new FloomError(`That's a directory, not a file: ${target}`);
56
+ }
57
+ throw new FloomError(`Couldn't create ${target}: ${err.message}`);
58
+ }
47
59
  process.stdout.write(`\n${symbols.ok} Created ${c.bold(basename(filePath))}\n`);
48
- process.stdout.write(` ${c.dim("Fill in the title + description, then run:")}\n`);
49
- process.stdout.write(` ${c.cyan(`floom publish ${target}`)}\n\n`);
60
+ process.stdout.write(`\n ${c.bold("Next")}\n`);
61
+ process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
62
+ process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`floom scan ${shellQuote(target)}`)}\n`);
63
+ process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`floom publish ${shellQuote(target)} --type instruction --public`)}\n\n`);
64
+ }
65
+ function shellQuote(value) {
66
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
67
+ return value;
68
+ return `'${value.replace(/'/g, "'\\''")}'`;
50
69
  }
51
70
  async function fileExists(p) {
52
71
  try {