@floomhq/floom 1.0.2 → 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,77 +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.2";
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.cyan("floom init")} ${c.dim("support-tone.md")}
38
- ${c.dim("Create a starter Markdown skill.")}
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.cyan("floom add")} ${c.dim("https://floom.dev/s/ffas93ud")}
41
- ${c.dim("Install a public skill. No account needed.")}
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")}
42
49
 
43
- ${c.cyan("floom publish")} ${c.dim("./support-tone.md --type instruction --public")}
44
- ${c.dim("Publish Markdown and print a share link.")}
50
+ ${c.bold("Safety")}
51
+ ${c.yellow("!")} ${c.dim("publish scans for API keys, prompt injection, and exfiltration.")}
45
52
 
46
- ${c.bold("Commands")}
47
- ${c.dim("Receive")}
48
- ${c.cyan("add")} ${c.dim("<url-or-slug>")} Install into ~/.claude/skills/
49
- ${c.cyan("info")} ${c.dim("<url-or-slug>")} Show metadata ${c.dim("[--json]")}
50
- ${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]")}
51
63
 
52
- ${c.dim("Create")}
53
- ${c.cyan("init")} ${c.dim("[file.md]")} Create a starter skill file
54
- ${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
55
71
 
56
- ${c.dim("Publish Flags")}
57
- ${c.dim("--public | --private | --unlisted")} ${c.dim("Set link visibility")}
58
- ${c.dim("--type <kind>")} ${c.dim("knowledge | instruction | workflow | skill")}
59
- ${c.dim("--installs-as <target>")} ${c.dim("Set install target metadata")}
60
- ${c.dim("claude_skill | memory | rule | codex_instruction")}
61
- ${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>")}
62
78
 
63
79
  ${c.dim("Account")}
64
- ${c.cyan("login")} Sign in with Google
65
- ${c.cyan("list")} List your published skills ${c.dim("[--json]")}
66
- ${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")}
67
96
  ${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
68
- ${c.cyan("delete")} ${c.dim("<url-or-slug>")} Delete one of your skills ${c.dim("[--yes]")}
69
- ${c.cyan("whoami")} Show the signed-in account
70
- ${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
71
100
 
72
- ${c.dim("System")}
73
- ${c.cyan("setup")} ${c.dim("[--target claude|codex] [--file path]")} Add Floom guidance to agent instructions
74
- ${c.cyan("connect")} ${c.dim("[--target claude|codex]")} Alias for setup
75
- ${c.cyan("mcp")} Print MCP setup guidance
76
- ${c.cyan("sync")} Preview: pull published, saved, and library skills
77
- ${c.cyan("watch")} Preview: poll published, saved, and library skills ${c.dim("[--interval <seconds>, min 10]")}
78
- ${c.cyan("doctor")} Diagnose auth, API, and local setup
79
- ${c.cyan("--help")} Show this help
80
- ${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")}
81
105
 
82
- ${c.bold("Env")}
83
- ${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
84
110
 
85
- ${c.bold("Links")}
86
- ${c.dim("Docs")} https://floom.dev
87
- ${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.")}
88
112
  `;
89
113
  process.stdout.write(out);
90
114
  }
@@ -110,14 +134,17 @@ function readFlagValue(argv, index, flag) {
110
134
  }
111
135
  function parseFlags(argv) {
112
136
  const out = { visibility: "unlisted", update: false, rest: [] };
137
+ let visibilityFlag = null;
113
138
  for (let i = 0; i < argv.length; i++) {
114
139
  const a = argv[i] ?? "";
115
- if (a === "--public")
116
- out.visibility = "public";
117
- else if (a === "--private")
118
- out.visibility = "private";
119
- else if (a === "--unlisted")
120
- 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
+ }
121
148
  else if (a === "--update") {
122
149
  throw new FloomError(V1_NOT_AVAILABLE, "`floom publish --update` is planned for a later Floom release.");
123
150
  }
@@ -140,19 +167,36 @@ function parseFlags(argv) {
140
167
  out.installsAs = value;
141
168
  i = nextIndex;
142
169
  }
143
- else if (a === "--version" || a.startsWith("--version=")) {
144
- 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);
145
173
  if (!VERSION_RE.test(value)) {
146
- 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.");
147
175
  }
148
176
  out.version = value;
149
177
  i = nextIndex;
150
178
  }
179
+ else if (a.startsWith("--")) {
180
+ throw new FloomError(`Unknown flag: ${a}`, "Try `floom publish skill.md --type instruction --public`.");
181
+ }
151
182
  else
152
183
  out.rest.push(a);
153
184
  }
154
185
  return out;
155
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
+ }
156
200
  function parseListFlags(argv) {
157
201
  const out = { json: false };
158
202
  for (const a of argv) {
@@ -161,6 +205,9 @@ function parseListFlags(argv) {
161
205
  else if (a.startsWith("--")) {
162
206
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom list --help` for usage.");
163
207
  }
208
+ else {
209
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom list --json`.");
210
+ }
164
211
  }
165
212
  return out;
166
213
  }
@@ -173,9 +220,38 @@ function parseInfoFlags(argv) {
173
220
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom info <slug> --json`.");
174
221
  else if (!out.slug)
175
222
  out.slug = a;
223
+ else
224
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom info <slug> --json`.");
176
225
  }
177
226
  return out;
178
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
+ }
179
255
  function parseSearchFlags(argv) {
180
256
  const out = { json: false };
181
257
  const terms = [];
@@ -215,9 +291,19 @@ function parseDeleteFlags(argv) {
215
291
  throw new FloomError(`Unknown flag: ${a}`, "Try `floom delete <slug> --yes`.");
216
292
  else if (!out.slug)
217
293
  out.slug = a;
294
+ else
295
+ throw new FloomError(`Unexpected argument: ${a}`, "Try `floom delete <slug> --yes`.");
218
296
  }
219
297
  return out;
220
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
+ }
221
307
  function parseSetupFlags(argv) {
222
308
  const out = { dryRun: false, yes: false };
223
309
  for (let i = 0; i < argv.length; i++) {
@@ -341,6 +427,9 @@ async function runLibrary(argv) {
341
427
  if (!librarySlug || !skillSlug) {
342
428
  throw new FloomError("Missing library or skill slug.", "Try `floom library add team-onboarding support-tone --folder support`.");
343
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
+ }
344
433
  await libraryAddSkill({
345
434
  librarySlug,
346
435
  skillSlug,
@@ -355,6 +444,9 @@ async function runLibrary(argv) {
355
444
  if (!librarySlug || !skillSlug) {
356
445
  throw new FloomError("Missing library or skill slug.", "Try `floom library remove team-onboarding support-tone`.");
357
446
  }
447
+ if (rest.length > 2) {
448
+ throw new FloomError(`Unexpected argument: ${rest[2]}`, "Try `floom library remove team-onboarding support-tone`.");
449
+ }
358
450
  await libraryRemoveSkill(librarySlug, skillSlug);
359
451
  return;
360
452
  }
@@ -362,6 +454,9 @@ async function runLibrary(argv) {
362
454
  const slug = rest[0];
363
455
  if (!slug)
364
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
+ }
365
460
  await librarySubscribe(slug);
366
461
  return;
367
462
  }
@@ -369,6 +464,9 @@ async function runLibrary(argv) {
369
464
  const slug = rest[0];
370
465
  if (!slug)
371
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
+ }
372
470
  await libraryUnsubscribe(slug);
373
471
  return;
374
472
  }
@@ -401,6 +499,19 @@ function parseWatchFlags(argv) {
401
499
  function notAvailable(feature) {
402
500
  throw new FloomError(V1_NOT_AVAILABLE, `${feature} is planned for a later Floom release.`);
403
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
+ }
404
515
  function sleep(ms, signal) {
405
516
  if (signal.aborted)
406
517
  return Promise.resolve();
@@ -456,28 +567,37 @@ async function main() {
456
567
  try {
457
568
  switch (cmd) {
458
569
  case undefined:
570
+ usage();
571
+ return;
459
572
  case "--help":
460
573
  case "-h":
461
574
  case "help":
462
- usage();
575
+ commandUsage();
576
+ return;
577
+ case "commands":
578
+ rejectArgs(rest, "Try `floom commands`.");
579
+ commandUsage();
463
580
  return;
464
581
  case "--version":
465
582
  case "-v":
466
- process.stdout.write(`${VERSION}\n`);
583
+ process.stdout.write(`${CLI_VERSION}\n`);
467
584
  return;
468
585
  case "login":
586
+ rejectArgs(rest, "Try `floom login`.");
469
587
  await login();
470
588
  return;
471
589
  case "logout":
590
+ rejectArgs(rest, "Try `floom logout`.");
472
591
  await deleteConfig();
473
592
  process.stdout.write(`\n${symbols.ok} Signed out\n\n`);
474
593
  return;
475
594
  case "whoami":
595
+ rejectArgs(rest, "Try `floom whoami`.");
476
596
  await whoami();
477
597
  return;
478
598
  case "init": {
479
- const file = rest[0];
480
- await init(file);
599
+ const flags = parseInitArgs(rest);
600
+ await init(flags.file);
481
601
  return;
482
602
  }
483
603
  case "publish": {
@@ -486,6 +606,9 @@ async function main() {
486
606
  if (!file) {
487
607
  throw new FloomError("Missing file argument.", "Try: `floom publish skill.md`");
488
608
  }
609
+ if (flags.rest.length > 1) {
610
+ throw new FloomError(`Unexpected argument: ${flags.rest[1]}`, "Try: `floom publish skill.md`");
611
+ }
489
612
  await publish({
490
613
  file,
491
614
  visibility: flags.visibility,
@@ -496,6 +619,11 @@ async function main() {
496
619
  });
497
620
  return;
498
621
  }
622
+ case "scan": {
623
+ const file = parseSingleFileArg(rest, "Try `floom scan skill.md`.");
624
+ await scanSkill(file);
625
+ return;
626
+ }
499
627
  case "share":
500
628
  notAvailable("`floom share`");
501
629
  case "list": {
@@ -524,14 +652,12 @@ async function main() {
524
652
  }
525
653
  case "add":
526
654
  case "install": {
527
- const slug = rest.find((a) => !a.startsWith("--"));
528
- if (!slug) {
529
- throw new FloomError("Missing skill slug.", "Try: `floom add <url-or-slug>`");
530
- }
531
- await install(slug);
655
+ const flags = parseAddArgs(rest);
656
+ await install(flags.slug, flags.target ? { target: flags.target } : {});
532
657
  return;
533
658
  }
534
659
  case "sync":
660
+ rejectArgs(rest, "Try `floom sync`.");
535
661
  await sync();
536
662
  return;
537
663
  case "setup":
@@ -564,13 +690,18 @@ async function main() {
564
690
  if (flags.folder === undefined) {
565
691
  throw new FloomError("Missing --folder.", "Use --folder <path> or --root. Add --tag or --tags when useful.");
566
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
+ }
567
696
  await moveSkill({ slug, folder: flags.folder, tags: flags.tags });
568
697
  return;
569
698
  }
570
699
  case "mcp":
700
+ rejectArgs(rest, "Try `floom mcp`.");
571
701
  printMcpSetup();
572
702
  return;
573
703
  case "doctor":
704
+ rejectArgs(rest, "Try `floom doctor`.");
574
705
  await doctor();
575
706
  return;
576
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 {